mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +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 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
|
||||
|
||||
|
@ -313,6 +313,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
case textMention(EnginePeer.Id)
|
||||
case textUrl(String)
|
||||
case customEmoji(stickerPack: StickerPackReference?, fileId: Int64)
|
||||
case strikethrough
|
||||
case underline
|
||||
case spoiler
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
@ -334,6 +337,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s")
|
||||
let fileId = try container.decode(Int64.self, forKey: "f")
|
||||
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
|
||||
case 6:
|
||||
self = .strikethrough
|
||||
case 7:
|
||||
self = .underline
|
||||
case 8:
|
||||
self = .spoiler
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .bold
|
||||
@ -359,6 +368,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
try container.encode(5 as Int32, forKey: "t")
|
||||
try container.encodeIfPresent(stickerPack, forKey: "s")
|
||||
try container.encode(fileId, forKey: "f")
|
||||
case .strikethrough:
|
||||
try container.encode(6 as Int32, forKey: "t")
|
||||
case .underline:
|
||||
try container.encode(7 as Int32, forKey: "t")
|
||||
case .spoiler:
|
||||
try container.encode(8 as Int32, forKey: "t")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -426,6 +441,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.strikethrough {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.underline {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.spoiler {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length)))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -464,6 +485,12 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case let .customEmoji(_, fileId):
|
||||
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case .strikethrough:
|
||||
result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case .underline:
|
||||
result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case .spoiler:
|
||||
result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
@ -678,6 +678,7 @@ public class AttachmentController: ViewController {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
|
||||
self?.animating = false
|
||||
self?.layer.removeAllAnimations()
|
||||
})
|
||||
} else {
|
||||
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
|
||||
@ -745,12 +746,12 @@ public class AttachmentController: ViewController {
|
||||
let position: CGPoint
|
||||
let positionY = layout.size.height - size.height - insets.bottom - 40.0
|
||||
if let sourceRect = controller.getSourceRect?() {
|
||||
position = CGPoint(x: floor(sourceRect.midX - size.width / 2.0), y: min(positionY, sourceRect.minY - size.height))
|
||||
position = CGPoint(x: min(layout.size.width - size.width - 28.0, floor(sourceRect.midX - size.width / 2.0)), y: min(positionY, sourceRect.minY - size.height))
|
||||
} else {
|
||||
position = CGPoint(x: masterWidth - 174.0, y: positionY)
|
||||
}
|
||||
|
||||
if controller.isStandalone {
|
||||
if controller.isStandalone && !controller.forceSourceRect {
|
||||
var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0)
|
||||
if let inputHeight = layout.inputHeight, inputHeight > 88.0 {
|
||||
containerY = layout.size.height - inputHeight - size.height - 80.0
|
||||
@ -933,6 +934,8 @@ public class AttachmentController: ViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public var forceSourceRect = false
|
||||
|
||||
fileprivate var isStandalone: Bool {
|
||||
return self.buttons.contains(.standalone)
|
||||
}
|
||||
|
@ -37,12 +37,15 @@ final class CameraDeviceContext {
|
||||
private weak var session: CameraSession?
|
||||
private weak var previewView: CameraSimplePreviewView?
|
||||
|
||||
private let exclusive: Bool
|
||||
|
||||
let device = CameraDevice()
|
||||
let input = CameraInput()
|
||||
let output = CameraOutput()
|
||||
|
||||
init(session: CameraSession) {
|
||||
init(session: CameraSession, exclusive: Bool) {
|
||||
self.session = session
|
||||
self.exclusive = exclusive
|
||||
}
|
||||
|
||||
func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) {
|
||||
@ -81,6 +84,9 @@ final class CameraDeviceContext {
|
||||
}
|
||||
|
||||
private var preferredMaxFrameRate: Double {
|
||||
if !self.exclusive {
|
||||
return 30.0
|
||||
}
|
||||
switch DeviceModel.current {
|
||||
case .iPhone14ProMax, .iPhone13ProMax:
|
||||
return 60.0
|
||||
@ -95,7 +101,7 @@ private final class CameraContext {
|
||||
|
||||
private let session: CameraSession
|
||||
|
||||
private let mainDeviceContext: CameraDeviceContext
|
||||
private var mainDeviceContext: CameraDeviceContext
|
||||
private var additionalDeviceContext: CameraDeviceContext?
|
||||
|
||||
private let cameraImageContext = CIContext()
|
||||
@ -162,7 +168,7 @@ private final class CameraContext {
|
||||
self.simplePreviewView = previewView
|
||||
self.secondaryPreviewView = secondaryPreviewView
|
||||
|
||||
self.mainDeviceContext = CameraDeviceContext(session: session)
|
||||
self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true)
|
||||
self.configure {
|
||||
self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata)
|
||||
}
|
||||
@ -306,9 +312,29 @@ private final class CameraContext {
|
||||
self.modeChange = .dualCamera
|
||||
if enabled {
|
||||
self.configure {
|
||||
self.additionalDeviceContext = CameraDeviceContext(session: self.session)
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false)
|
||||
self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
|
||||
self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false)
|
||||
self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false)
|
||||
}
|
||||
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.previewNode?.enqueue(sampleBuffer)
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
var mirror = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
return
|
||||
@ -325,9 +351,29 @@ private final class CameraContext {
|
||||
}
|
||||
} else {
|
||||
self.configure {
|
||||
self.mainDeviceContext.invalidate()
|
||||
self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true)
|
||||
self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata)
|
||||
|
||||
self.additionalDeviceContext?.invalidate()
|
||||
self.additionalDeviceContext = nil
|
||||
}
|
||||
self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.previewNode?.enqueue(sampleBuffer)
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
var mirror = false
|
||||
if #available(iOS 13.0, *) {
|
||||
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
|
||||
}
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.queue.after(0.5) {
|
||||
@ -394,11 +440,33 @@ private final class CameraContext {
|
||||
}
|
||||
|
||||
public func startRecording() -> Signal<Double, NoError> {
|
||||
return self.mainDeviceContext.output.startRecording()
|
||||
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||
return combineLatest(
|
||||
self.mainDeviceContext.output.startRecording(),
|
||||
additionalDeviceContext.output.startRecording()
|
||||
) |> map { value, _ in
|
||||
return value
|
||||
}
|
||||
} else {
|
||||
return self.mainDeviceContext.output.startRecording()
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
return self.mainDeviceContext.output.stopRecording()
|
||||
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||
if let additionalDeviceContext = self.additionalDeviceContext {
|
||||
return combineLatest(
|
||||
self.mainDeviceContext.output.stopRecording(),
|
||||
additionalDeviceContext.output.stopRecording()
|
||||
) |> mapToSignal { main, additional in
|
||||
if case let .finished(mainResult, _, _) = main, case let .finished(additionalResult, _, _) = additional {
|
||||
return .single(.finished(mainResult, additionalResult, CACurrentMediaTime()))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return self.mainDeviceContext.output.stopRecording()
|
||||
}
|
||||
}
|
||||
|
||||
var detectedCodes: Signal<[CameraCode], NoError> {
|
||||
@ -559,7 +627,7 @@ public final class Camera {
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
public func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.queue.async {
|
||||
|
@ -6,6 +6,28 @@ import CoreImage
|
||||
import Vision
|
||||
import VideoToolbox
|
||||
|
||||
public enum VideoCaptureResult: Equatable {
|
||||
case finished((String, UIImage), (String, UIImage)?, Double)
|
||||
case failed
|
||||
|
||||
public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool {
|
||||
switch lhs {
|
||||
case .failed:
|
||||
if case .failed = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .finished(_, _, lhsTime):
|
||||
if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CameraCode: Equatable {
|
||||
public enum CodeType {
|
||||
case qr
|
||||
@ -272,7 +294,7 @@ final class CameraOutput: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private var recordingCompletionPipe = ValuePipe<(String, UIImage?)?>()
|
||||
private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
|
||||
func startRecording() -> Signal<Double, NoError> {
|
||||
guard self.videoRecorder == nil else {
|
||||
return .complete()
|
||||
@ -288,18 +310,16 @@ final class CameraOutput: NSObject {
|
||||
guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else {
|
||||
return .complete()
|
||||
}
|
||||
guard let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) else {
|
||||
return .complete()
|
||||
}
|
||||
let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) ?? [:]
|
||||
|
||||
let outputFileName = NSUUID().uuidString
|
||||
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
|
||||
let outputFileURL = URL(fileURLWithPath: outputFilePath)
|
||||
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in
|
||||
if case let .success(transitionImage) = result {
|
||||
self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage))
|
||||
self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, CACurrentMediaTime()))
|
||||
} else {
|
||||
self?.recordingCompletionPipe.putNext(nil)
|
||||
self?.recordingCompletionPipe.putNext(.failed)
|
||||
}
|
||||
})
|
||||
|
||||
@ -318,7 +338,7 @@ final class CameraOutput: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
func stopRecording() -> Signal<(String, UIImage?)?, NoError> {
|
||||
func stopRecording() -> Signal<VideoCaptureResult, NoError> {
|
||||
self.videoRecorder?.stop()
|
||||
|
||||
return self.recordingCompletionPipe.signal()
|
||||
|
@ -1191,18 +1191,19 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
|
||||
}
|
||||
|
||||
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
|
||||
if selectedIndex <= 0 && translation.x > 0.0 {
|
||||
transitionFraction = 0.0
|
||||
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
|
||||
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
|
||||
}
|
||||
|
||||
if cameraIsAlreadyOpened {
|
||||
transitionFraction = 0.0
|
||||
return
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
|
||||
if selectedIndex <= 0 && translation.x > 0.0 {
|
||||
transitionFraction = 0.0
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
|
||||
} else if translation.x <= 0.0 && cameraIsAlreadyOpened {
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
|
||||
}
|
||||
|
||||
if cameraIsAlreadyOpened {
|
||||
transitionFraction = 0.0
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
|
||||
|
@ -321,7 +321,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
if setup {
|
||||
text.referenceDrawingSize = self.size
|
||||
text.width = floor(self.size.width * 0.9)
|
||||
text.fontSize = 0.3
|
||||
text.fontSize = 0.08
|
||||
text.scale = zoomScale
|
||||
}
|
||||
}
|
||||
@ -415,9 +415,16 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
let newEntity = entity.duplicate()
|
||||
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
||||
|
||||
guard let view = makeEntityView(context: self.context, entity: entity) else {
|
||||
guard let view = makeEntityView(context: self.context, entity: newEntity) else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
if let initialView = self.getView(for: entity.uuid) {
|
||||
view.onSnapUpdated = initialView.onSnapUpdated
|
||||
view.onPositionUpdated = initialView.onPositionUpdated
|
||||
view.onInteractionUpdated = initialView.onInteractionUpdated
|
||||
}
|
||||
|
||||
view.containerView = self
|
||||
view.update()
|
||||
self.addSubview(view)
|
||||
@ -516,6 +523,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func eachView(_ f: (DrawingEntityView) -> Void) {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
f(view)
|
||||
}
|
||||
}
|
||||
|
||||
public func play() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.play()
|
||||
@ -700,15 +713,15 @@ public class DrawingEntityView: UIView {
|
||||
return self.bounds
|
||||
}
|
||||
|
||||
func play() {
|
||||
public func play() {
|
||||
|
||||
}
|
||||
|
||||
func pause() {
|
||||
public func pause() {
|
||||
|
||||
}
|
||||
|
||||
func seek(to timestamp: Double) {
|
||||
public func seek(to timestamp: Double) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -43,17 +43,17 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
}
|
||||
}
|
||||
|
||||
override func play() {
|
||||
public override func play() {
|
||||
self.isVisible = true
|
||||
self.applyVisibility()
|
||||
}
|
||||
|
||||
override func pause() {
|
||||
public override func pause() {
|
||||
self.isVisible = false
|
||||
self.applyVisibility()
|
||||
}
|
||||
|
||||
override func seek(to timestamp: Double) {
|
||||
public override func seek(to timestamp: Double) {
|
||||
self.isVisible = false
|
||||
self.isPlaying = false
|
||||
|
||||
|
@ -738,7 +738,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
areUnicodeEmojiEnabled: true,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
|
||||
@ -749,7 +749,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
hasTrending: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
@ -761,7 +761,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
|
||||
stickerOrderedItemListCollectionIds: [],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
hasTrending: false,
|
||||
forceHasPremium: true
|
||||
)
|
||||
@ -1153,8 +1153,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
controlsAreVisible = false
|
||||
}
|
||||
|
||||
let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
|
||||
let previewSize: CGSize
|
||||
let previewTopInset: CGFloat = environment.statusBarHeight + 12.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
let previewHeight = context.availableSize.height - previewTopInset - 75.0
|
||||
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
|
||||
} else {
|
||||
previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
|
||||
}
|
||||
let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset
|
||||
|
||||
var topInset = environment.safeInsets.top + 31.0
|
||||
@ -1646,7 +1652,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(fillButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
@ -1678,7 +1684,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(flipButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||
@ -1988,6 +1994,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
doneButtonPosition.x = doneButtonPosition.x - 2.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
doneButtonPosition.x -= 20.0
|
||||
}
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0)
|
||||
}
|
||||
context.add(doneButton
|
||||
@ -2105,6 +2114,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
backButtonPosition.x = backButtonPosition.x + 2.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
backButtonPosition.x += 20.0
|
||||
}
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0)
|
||||
}
|
||||
context.add(backButton
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
@ -21,6 +22,10 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
|
||||
private var videoPlayer: AVPlayer?
|
||||
private var videoLayer: AVPlayerLayer?
|
||||
private var videoImageView: UIImageView?
|
||||
|
||||
private var didSetUpAnimationNode = false
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
private let cachedDisposable = MetaDisposable()
|
||||
@ -63,12 +68,27 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
}
|
||||
}
|
||||
|
||||
private var video: String? {
|
||||
if case let .video(path, _) = self.stickerEntity.content {
|
||||
return path
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var dimensions: CGSize {
|
||||
switch self.stickerEntity.content {
|
||||
case let .file(file):
|
||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case let .image(image):
|
||||
return image.size
|
||||
case let .file(file):
|
||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case let .image(image):
|
||||
return image.size
|
||||
case let .video(_, image):
|
||||
if let image {
|
||||
let minSide = min(image.size.width, image.size.height)
|
||||
return CGSize(width: minSide, height: minSide)
|
||||
} else {
|
||||
return CGSize(width: 512.0, height: 512.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,23 +139,64 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
return context
|
||||
}))
|
||||
self.setNeedsLayout()
|
||||
} else if case let .video(videoPath, image) = self.stickerEntity.content {
|
||||
let url = URL(fileURLWithPath: videoPath)
|
||||
let asset = AVURLAsset(url: url)
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
player.automaticallyWaitsToMinimizeStalling = false
|
||||
let layer = AVPlayerLayer(player: player)
|
||||
layer.masksToBounds = true
|
||||
layer.videoGravity = .resizeAspectFill
|
||||
|
||||
self.layer.addSublayer(layer)
|
||||
|
||||
self.videoPlayer = player
|
||||
self.videoLayer = layer
|
||||
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
self.addSubview(imageView)
|
||||
self.videoImageView = imageView
|
||||
}
|
||||
}
|
||||
|
||||
override func play() {
|
||||
self.isVisible = true
|
||||
self.applyVisibility()
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.play()
|
||||
|
||||
if let videoImageView = self.videoImageView {
|
||||
self.videoImageView = nil
|
||||
Queue.mainQueue().after(0.1) {
|
||||
videoImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak videoImageView] _ in
|
||||
videoImageView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func pause() {
|
||||
self.isVisible = false
|
||||
self.applyVisibility()
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override func seek(to timestamp: Double) {
|
||||
self.isVisible = false
|
||||
self.isPlaying = false
|
||||
self.animationNode?.seekTo(.timestamp(timestamp))
|
||||
|
||||
if let player = self.videoPlayer {
|
||||
player.seek(to: CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
override func resetToStart() {
|
||||
@ -184,10 +245,11 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
let boundingSize = CGSize(width: sideSize, height: sideSize)
|
||||
|
||||
let imageSize = self.dimensions.aspectFitted(boundingSize)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.frame = imageFrame
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
|
||||
if !self.didApplyVisibility {
|
||||
@ -195,6 +257,16 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
self.applyVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
if let videoLayer = self.videoLayer {
|
||||
videoLayer.cornerRadius = imageFrame.width / 2.0
|
||||
videoLayer.frame = imageFrame
|
||||
}
|
||||
if let videoImageView = self.videoImageView {
|
||||
videoImageView.layer.cornerRadius = imageFrame.width / 2.0
|
||||
videoImageView.frame = imageFrame
|
||||
}
|
||||
|
||||
self.update(animated: false)
|
||||
}
|
||||
}
|
||||
@ -226,13 +298,19 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.imageNode.transform = animationTargetTransform
|
||||
self.animationNode?.transform = animationTargetTransform
|
||||
self.videoLayer?.transform = animationTargetTransform
|
||||
}, completion: { finished in
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoLayer?.transform = staticTransform
|
||||
})
|
||||
} else {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
self.imageNode.transform = staticTransform
|
||||
self.animationNode?.transform = staticTransform
|
||||
self.videoLayer?.transform = staticTransform
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
super.update(animated: animated)
|
||||
|
@ -35,6 +35,7 @@ public struct StickerPickerInputData: Equatable {
|
||||
private final class StickerSelectionComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let deviceMetrics: DeviceMetrics
|
||||
@ -44,6 +45,7 @@ private final class StickerSelectionComponent: Component {
|
||||
let separatorColor: UIColor
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
@ -52,6 +54,7 @@ private final class StickerSelectionComponent: Component {
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.deviceMetrics = deviceMetrics
|
||||
@ -129,6 +132,7 @@ private final class StickerSelectionComponent: Component {
|
||||
|
||||
let topPanelHeight: CGFloat = 42.0
|
||||
|
||||
//let context = component.context
|
||||
let keyboardSize = self.keyboardView.update(
|
||||
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
|
||||
component: AnyComponent(EntityKeyboardComponent(
|
||||
@ -153,7 +157,44 @@ private final class StickerSelectionComponent: Component {
|
||||
switchToTextInput: {},
|
||||
switchToGifSubject: { _ in },
|
||||
reorderItems: { _, _ in },
|
||||
makeSearchContainerNode: { _ in return nil },
|
||||
makeSearchContainerNode: { _ in
|
||||
return nil
|
||||
},
|
||||
// makeSearchContainerNode: { [weak self, weak controllerInteraction] content in
|
||||
// guard let self, let controllerInteraction = controllerInteraction else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let mappedMode: ChatMediaInputSearchMode
|
||||
// switch content {
|
||||
// case .stickers:
|
||||
// mappedMode = .sticker
|
||||
// case .gifs:
|
||||
// mappedMode = .sticker
|
||||
// }
|
||||
//
|
||||
// let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
// let searchContainerNode = PaneSearchContainerNode(
|
||||
// context: context,
|
||||
// theme: presentationData.theme,
|
||||
// strings: presentationData.strings,
|
||||
// controllerInteraction: controllerInteraction,
|
||||
// inputNodeInteraction: inputNodeInteraction,
|
||||
// mode: mappedMode,
|
||||
// trendingGifsPromise: Promise(nil),
|
||||
// cancel: {
|
||||
// },
|
||||
// peekBehavior: self.emojiInputInteraction?.peekBehavior
|
||||
// )
|
||||
// searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved)
|
||||
// }
|
||||
//
|
||||
// return searchContainerNode
|
||||
// },
|
||||
contentIdUpdated: { _ in },
|
||||
deviceMetrics: component.deviceMetrics,
|
||||
hiddenInputHeight: 0.0,
|
||||
@ -225,6 +266,39 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
fileprivate var temporaryDismiss = false
|
||||
|
||||
private struct EmojiSearchResult {
|
||||
var groups: [EmojiPagerContentComponent.ItemGroup]
|
||||
var id: AnyHashable
|
||||
var version: Int
|
||||
var isPreset: Bool
|
||||
}
|
||||
|
||||
private struct EmojiSearchState {
|
||||
var result: EmojiSearchResult?
|
||||
var isSearching: Bool
|
||||
|
||||
init(result: EmojiSearchResult?, isSearching: Bool) {
|
||||
self.result = result
|
||||
self.isSearching = isSearching
|
||||
}
|
||||
}
|
||||
|
||||
private let emojiSearchDisposable = MetaDisposable()
|
||||
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
|
||||
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
|
||||
didSet {
|
||||
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
|
||||
}
|
||||
}
|
||||
|
||||
private let stickerSearchDisposable = MetaDisposable()
|
||||
private let stickerSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
|
||||
private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
|
||||
didSet {
|
||||
self.stickerSearchState.set(.single(self.stickerSearchStateValue))
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller = controller
|
||||
@ -249,8 +323,52 @@ public class StickerPickerScreen: ViewController {
|
||||
self.wrappingView.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.hostView)
|
||||
|
||||
self.contentDisposable.set(controller.inputData.start(next: { [weak self] inputData in
|
||||
let signal = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
controller.inputData,
|
||||
self.stickerSearchState.get(),
|
||||
self.emojiSearchState.get()
|
||||
)
|
||||
|
||||
|
||||
self.contentDisposable.set(signal.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in
|
||||
if let strongSelf = self {
|
||||
let presentationData = strongSelf.presentationData
|
||||
var inputData = inputData
|
||||
|
||||
let emoji = inputData.emoji
|
||||
if let emojiSearchResult = emojiSearchState.result {
|
||||
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
||||
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
||||
text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
|
||||
iconFile: nil
|
||||
)
|
||||
}
|
||||
let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true)
|
||||
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState)
|
||||
} else if emojiSearchState.isSearching {
|
||||
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching)
|
||||
}
|
||||
|
||||
if let stickerSearchResult = stickerSearchState.result {
|
||||
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
|
||||
stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
||||
text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult,
|
||||
iconFile: nil
|
||||
)
|
||||
}
|
||||
if let stickers = inputData.stickers {
|
||||
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true)
|
||||
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState)
|
||||
}
|
||||
} else if stickerSearchState.isSearching {
|
||||
if let stickers = inputData.stickers {
|
||||
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.updateContent(inputData)
|
||||
}
|
||||
}))
|
||||
@ -258,6 +376,8 @@ public class StickerPickerScreen: ViewController {
|
||||
|
||||
deinit {
|
||||
self.contentDisposable.dispose()
|
||||
self.emojiSearchDisposable.dispose()
|
||||
self.stickerSearchDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateContent(_ content: StickerPickerInputData) {
|
||||
@ -363,9 +483,224 @@ public class StickerPickerScreen: ViewController {
|
||||
navigationController: { [weak self] in
|
||||
return self?.controller?.navigationController as? NavigationController
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
requestUpdate: { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if !transition.animation.isImmediate, let (layout, navigationHeight) = strongSelf.currentLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition)
|
||||
}
|
||||
},
|
||||
updateSearchQuery: { _ in
|
||||
updateSearchQuery: { [weak self] query in
|
||||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
switch query {
|
||||
case .none:
|
||||
self.emojiSearchDisposable.set(nil)
|
||||
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
|
||||
case let .text(rawQuery, languageCode):
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
self.emojiSearchDisposable.set(nil)
|
||||
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
|
||||
} else {
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hasPremium: Signal<Bool, NoError> = .single(true)
|
||||
let resultSignal = combineLatest(
|
||||
signal,
|
||||
hasPremium
|
||||
)
|
||||
|> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError>
|
||||
if hasPremium {
|
||||
remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys))
|
||||
} else {
|
||||
remoteSignal = .single(([], true))
|
||||
}
|
||||
return remoteSignal
|
||||
|> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult {
|
||||
return .complete()
|
||||
}
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
let appendUnicodeEmoji = {
|
||||
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
|
||||
for emojiString in list {
|
||||
if allEmoticons[emojiString] != nil {
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: nil,
|
||||
content: .staticEmoji(emojiString),
|
||||
itemFile: nil,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPremium {
|
||||
appendUnicodeEmoji()
|
||||
}
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for itemFile in foundEmoji.items {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
if itemFile.isPremiumEmoji && !hasPremium {
|
||||
continue
|
||||
}
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile,
|
||||
subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
if hasPremium {
|
||||
appendUnicodeEmoji()
|
||||
}
|
||||
|
||||
return .single([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
var version = 0
|
||||
self.emojiSearchStateValue.isSearching = true
|
||||
self.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
|
||||
version += 1
|
||||
}))
|
||||
}
|
||||
case let .category(value):
|
||||
let resultSignal = context.engine.stickers.searchEmoji(emojiString: value)
|
||||
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for itemFile in files {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)], isFinalResult))
|
||||
}
|
||||
|
||||
var version = 0
|
||||
self.emojiSearchDisposable.set((resultSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let group = result.items.first else {
|
||||
return
|
||||
}
|
||||
if group.items.isEmpty && !result.isFinalResult {
|
||||
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
|
||||
EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: true,
|
||||
items: []
|
||||
)
|
||||
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
return
|
||||
}
|
||||
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
version += 1
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
@ -570,7 +905,95 @@ public class StickerPickerScreen: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _ in
|
||||
updateSearchQuery: { [weak self] query in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
switch query {
|
||||
case .none:
|
||||
strongSelf.stickerSearchDisposable.set(nil)
|
||||
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
|
||||
case .text:
|
||||
strongSelf.stickerSearchDisposable.set(nil)
|
||||
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
|
||||
case let .category(value):
|
||||
let resultSignal = context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote])
|
||||
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in files.items {
|
||||
let itemFile = item.file
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
return .single(([EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: false,
|
||||
items: items
|
||||
)], files.isFinalResult))
|
||||
}
|
||||
|
||||
var version = 0
|
||||
strongSelf.stickerSearchDisposable.set((resultSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let group = result.items.first else {
|
||||
return
|
||||
}
|
||||
if group.items.isEmpty && !result.isFinalResult {
|
||||
//strongSelf.stickerSearchStateValue.isSearching = true
|
||||
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
|
||||
EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
fillWithLoadingPlaceholders: true,
|
||||
items: []
|
||||
)
|
||||
], id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
return
|
||||
}
|
||||
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
|
||||
version += 1
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateScrollingToItemGroup: { [weak self] in
|
||||
self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
@ -667,6 +1090,9 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
self.currentLayout = (layout, navigationHeight)
|
||||
|
||||
self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0))
|
||||
@ -691,7 +1117,10 @@ public class StickerPickerScreen: ViewController {
|
||||
}
|
||||
transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil)
|
||||
|
||||
let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
|
||||
var modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset)
|
||||
if self.isDismissing {
|
||||
modalProgress = 0.0
|
||||
}
|
||||
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let clipFrame: CGRect
|
||||
@ -761,6 +1190,7 @@ public class StickerPickerScreen: ViewController {
|
||||
transition: stickersTransition,
|
||||
component: AnyComponent(
|
||||
StickerSelectionComponent(
|
||||
context: controller.context,
|
||||
theme: self.theme,
|
||||
strings: self.presentationData.strings,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
|
@ -146,6 +146,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity {
|
||||
case let .image(image):
|
||||
self.file = nil
|
||||
self.imagePromise.set(.single(image))
|
||||
case .video:
|
||||
self.file = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -668,6 +668,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem)
|
||||
self.enqueueTransaction(transaction)
|
||||
|
||||
if !self.didSetReady {
|
||||
updateLayout = true
|
||||
}
|
||||
|
||||
if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
@ -2167,6 +2171,7 @@ public func wallpaperMediaPickerController(
|
||||
|
||||
public func storyMediaPickerController(
|
||||
context: AccountContext,
|
||||
getSourceRect: @escaping () -> CGRect,
|
||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
|
||||
dismissed: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
@ -2175,6 +2180,8 @@ public func storyMediaPickerController(
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
controller.forceSourceRect = true
|
||||
controller.getSourceRect = getSourceRect
|
||||
controller.requestController = { _, present in
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil)
|
||||
mediaPickerController.customSelection = { controller, result in
|
||||
|
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 ComponentFlow
|
||||
|
||||
final class CameraButton: Component {
|
||||
public final class CameraButton: Component {
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let minSize: CGSize?
|
||||
let tag: AnyObject?
|
||||
let isEnabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
public init(
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
minSize: CGSize? = nil,
|
||||
tag: AnyObject? = nil,
|
||||
@ -23,7 +23,7 @@ final class CameraButton: Component {
|
||||
self.action = action
|
||||
}
|
||||
|
||||
func tagged(_ tag: AnyObject) -> CameraButton {
|
||||
public func tagged(_ tag: AnyObject) -> CameraButton {
|
||||
return CameraButton(
|
||||
content: self.content,
|
||||
minSize: self.minSize,
|
||||
@ -33,7 +33,7 @@ final class CameraButton: Component {
|
||||
)
|
||||
}
|
||||
|
||||
static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool {
|
||||
public static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool {
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
@ -49,8 +49,8 @@ final class CameraButton: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIButton, ComponentTaggedView {
|
||||
private var contentView: ComponentHostView<Empty>
|
||||
public final class View: UIButton, ComponentTaggedView {
|
||||
public var contentView: ComponentHostView<Empty>
|
||||
|
||||
private var component: CameraButton?
|
||||
private var currentIsHighlighted: Bool = false {
|
||||
@ -74,7 +74,7 @@ final class CameraButton: Component {
|
||||
transition.setScale(view: self, scale: scale)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
public override init(frame: CGRect) {
|
||||
self.contentView = ComponentHostView<Empty>()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
@ -104,19 +104,19 @@ final class CameraButton: Component {
|
||||
self.component?.action()
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
self.currentIsHighlighted = true
|
||||
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
public override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
self.currentIsHighlighted = false
|
||||
|
||||
super.endTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
public override func cancelTracking(with event: UIEvent?) {
|
||||
self.currentIsHighlighted = false
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
@ -155,11 +155,11 @@ final class CameraButton: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -73,7 +73,8 @@ swift_library(
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/Components/MetalImageView:MetalImageView",
|
||||
"//submodules/Components/MetalImageView",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -37,9 +37,9 @@ float sdfCircle(float2 uv, float2 position, float radius) {
|
||||
return length(uv - position) - radius;
|
||||
}
|
||||
|
||||
float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) {
|
||||
float primary = sdfRoundedRectangle(uv, float2(primaryParameters.y, 0.0), primaryParameters.x, primaryParameters.w);
|
||||
float secondary = sdfCircle(uv, float2(secondaryParameters.y, 0.0), secondaryParameters.x);
|
||||
float map(float2 uv, float3 primaryParameters, float2 primaryOffset, float3 secondaryParameters, float2 secondaryOffset) {
|
||||
float primary = sdfRoundedRectangle(uv, primaryOffset, primaryParameters.x, primaryParameters.z);
|
||||
float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x);
|
||||
float metaballs = 1.0;
|
||||
metaballs = smin(metaballs, primary, BindingDistance);
|
||||
metaballs = smin(metaballs, secondary, BindingDistance);
|
||||
@ -48,22 +48,32 @@ float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) {
|
||||
|
||||
fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]],
|
||||
constant uint2 &resolution[[buffer(0)]],
|
||||
constant float4 &primaryParameters[[buffer(1)]],
|
||||
constant float2 &secondaryParameters[[buffer(2)]])
|
||||
constant float3 &primaryParameters[[buffer(1)]],
|
||||
constant float2 &primaryOffset[[buffer(2)]],
|
||||
constant float3 &secondaryParameters[[buffer(3)]],
|
||||
constant float2 &secondaryOffset[[buffer(4)]])
|
||||
{
|
||||
float2 R = float2(resolution.x, resolution.y);
|
||||
float2 uv = (2.0 * in.position.xy - R.xy) / R.y;
|
||||
|
||||
float2 uv;
|
||||
float offset;
|
||||
if (R.x > R.y) {
|
||||
uv = (2.0 * in.position.xy - R.xy) / R.y;
|
||||
offset = uv.x;
|
||||
} else {
|
||||
uv = (2.0 * in.position.xy - R.xy) / R.x;
|
||||
offset = uv.y;
|
||||
}
|
||||
|
||||
float t = AARadius / resolution.y;
|
||||
|
||||
float cAlpha = 1.0 - primaryParameters.z;
|
||||
float cAlpha = 1.0 - primaryParameters.y;
|
||||
float bound = primaryParameters.x + 0.05;
|
||||
if (abs(uv.x) > bound) {
|
||||
cAlpha = mix(0.0, 1.0, min(1.0, (abs(uv.x) - bound) * 2.4));
|
||||
|
||||
if (abs(offset) > bound) {
|
||||
cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4));
|
||||
}
|
||||
|
||||
float c = smoothstep(t, -t, map(uv, primaryParameters, secondaryParameters));
|
||||
float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset));
|
||||
|
||||
return half4(c, max(cAlpha, 0.231), max(cAlpha, 0.188), c);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import LottieAnimationComponent
|
||||
import TooltipUI
|
||||
import MediaEditor
|
||||
import BundleIconComponent
|
||||
import CameraButtonComponent
|
||||
|
||||
let videoRedColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
@ -86,6 +87,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let camera: Camera
|
||||
let updateState: ActionSlot<CameraState>
|
||||
let hasAppeared: Bool
|
||||
let panelWidth: CGFloat
|
||||
let flipAnimationAction: ActionSlot<Void>
|
||||
let present: (ViewController) -> Void
|
||||
let push: (ViewController) -> Void
|
||||
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@ -95,6 +98,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
camera: Camera,
|
||||
updateState: ActionSlot<CameraState>,
|
||||
hasAppeared: Bool,
|
||||
panelWidth: CGFloat,
|
||||
flipAnimationAction: ActionSlot<Void>,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
push: @escaping (ViewController) -> Void,
|
||||
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@ -103,6 +108,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.camera = camera
|
||||
self.updateState = updateState
|
||||
self.hasAppeared = hasAppeared
|
||||
self.panelWidth = panelWidth
|
||||
self.flipAnimationAction = flipAnimationAction
|
||||
self.present = present
|
||||
self.push = push
|
||||
self.completion = completion
|
||||
@ -115,6 +122,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
if lhs.panelWidth != rhs.panelWidth {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -186,7 +196,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
})
|
||||
|
||||
Queue.mainQueue().async {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
self.setupRecentAssetSubscription()
|
||||
}
|
||||
}
|
||||
@ -229,9 +239,18 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
func togglePosition() {
|
||||
private var lastFlipTimestamp: Double?
|
||||
func togglePosition(_ action: ActionSlot<Void>) {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 {
|
||||
return
|
||||
}
|
||||
self.lastFlipTimestamp = currentTimestamp
|
||||
|
||||
self.camera.togglePosition()
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
action.invoke(Void())
|
||||
}
|
||||
|
||||
func toggleDualCamera() {
|
||||
@ -256,7 +275,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
case .began:
|
||||
return .single(.pendingImage)
|
||||
case let .finished(mainImage, additionalImage, _):
|
||||
return .single(.image(mainImage, additionalImage))
|
||||
return .single(.image(mainImage, additionalImage, .bottomRight))
|
||||
case .failed:
|
||||
return .complete()
|
||||
}
|
||||
@ -282,9 +301,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
func stopVideoRecording() {
|
||||
self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0)
|
||||
self.resultDisposable.set((self.camera.stopRecording()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in
|
||||
if let self, let (path, transitionImage) = pathAndTransitionImage {
|
||||
self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920))))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let self, case let .finished(mainResult, additionalResult, _) = result {
|
||||
self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), .bottomRight)))
|
||||
}
|
||||
}))
|
||||
self.isTransitioning = true
|
||||
@ -316,15 +335,13 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let zoomControl = Child(ZoomComponent.self)
|
||||
let flashButton = Child(CameraButton.self)
|
||||
let flipButton = Child(CameraButton.self)
|
||||
// let dualButton = Child(CameraButton.self)
|
||||
let dualButton = Child(CameraButton.self)
|
||||
let modeControl = Child(ModeComponent.self)
|
||||
let hintLabel = Child(HintLabelComponent.self)
|
||||
|
||||
let timeBackground = Child(RoundedRectangle.self)
|
||||
let timeLabel = Child(MultilineTextComponent.self)
|
||||
|
||||
let flipAnimationAction = ActionSlot<Void>()
|
||||
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let component = context.component
|
||||
@ -339,6 +356,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
let smallPanelWidth = min(component.panelWidth, 88.0)
|
||||
let panelWidth = min(component.panelWidth, 185.0)
|
||||
|
||||
let topControlInset: CGFloat = 20.0
|
||||
if case .none = state.cameraState.recording, !state.isTransitioning {
|
||||
let cancelButton = cancelButton.update(
|
||||
@ -363,7 +383,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(cancelButton
|
||||
.position(CGPoint(x: topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0))
|
||||
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
@ -423,36 +443,36 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(flashButton
|
||||
.position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0))
|
||||
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
|
||||
// if #available(iOS 13.0, *) {
|
||||
// let dualButton = dualButton.update(
|
||||
// component: CameraButton(
|
||||
// content: AnyComponentWithIdentity(
|
||||
// id: "dual",
|
||||
// component: AnyComponent(
|
||||
// DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
|
||||
// )
|
||||
// ),
|
||||
// action: { [weak state] in
|
||||
// guard let state else {
|
||||
// return
|
||||
// }
|
||||
// state.toggleDualCamera()
|
||||
// }
|
||||
// ).tagged(dualButtonTag),
|
||||
// availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
// transition: .immediate
|
||||
// )
|
||||
// context.add(dualButton
|
||||
// .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0))
|
||||
// .appear(.default(scale: true))
|
||||
// .disappear(.default(scale: true))
|
||||
// )
|
||||
// }
|
||||
if #available(iOS 13.0, *), !isTablet && !"".isEmpty {
|
||||
let dualButton = dualButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "dual",
|
||||
component: AnyComponent(
|
||||
DualIconComponent(isSelected: state.cameraState.isDualCamEnabled)
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
state.toggleDualCamera()
|
||||
}
|
||||
).tagged(dualButtonTag),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(dualButton
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if case .holding = state.cameraState.recording {
|
||||
@ -494,9 +514,17 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
let flipAnimationAction = component.flipAnimationAction
|
||||
let captureControlsAvailableSize: CGSize
|
||||
if isTablet {
|
||||
captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height)
|
||||
} else {
|
||||
captureControlsAvailableSize = availableSize
|
||||
}
|
||||
let captureControls = captureControls.update(
|
||||
component: CaptureControlsComponent(
|
||||
isTablet: isTablet,
|
||||
hasAppeared: component.hasAppeared,
|
||||
shutterState: shutterState,
|
||||
lastGalleryAsset: state.lastGalleryAsset,
|
||||
tag: captureControlsTag,
|
||||
@ -537,7 +565,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
state.togglePosition()
|
||||
state.togglePosition(flipAnimationAction)
|
||||
},
|
||||
galleryTapped: {
|
||||
guard let controller = environment.controller() as? CameraScreen else {
|
||||
@ -550,45 +578,50 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
},
|
||||
zoomUpdated: { fraction in
|
||||
state.updateZoom(fraction: fraction)
|
||||
}
|
||||
},
|
||||
flipAnimationAction: flipAnimationAction
|
||||
),
|
||||
availableSize: availableSize,
|
||||
availableSize: captureControlsAvailableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let captureControlsPosition: CGPoint
|
||||
if isTablet {
|
||||
captureControlsPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0)
|
||||
} else {
|
||||
captureControlsPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)
|
||||
}
|
||||
context.add(captureControls
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0))
|
||||
.position(captureControlsPosition)
|
||||
)
|
||||
|
||||
if isTablet {
|
||||
let flipButton = flipButton.update(
|
||||
component: CameraButton(
|
||||
component: CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "flip",
|
||||
component: AnyComponent(
|
||||
FlipButtonContentComponent(action: flipAnimationAction)
|
||||
FlipButtonContentComponent(
|
||||
action: flipAnimationAction,
|
||||
maskFrame: .zero
|
||||
)
|
||||
)
|
||||
),
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: {
|
||||
// let currentTimestamp = CACurrentMediaTime()
|
||||
// if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 {
|
||||
// return
|
||||
// }
|
||||
// self.lastFlipTimestamp = currentTimestamp
|
||||
state.togglePosition()
|
||||
flipAnimationAction.invoke(Void())
|
||||
state.togglePosition(flipAnimationAction)
|
||||
}
|
||||
),
|
||||
availableSize: availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(flipButton
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0))
|
||||
.position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
}
|
||||
|
||||
var isVideoRecording = false
|
||||
if case .video = state.cameraState.mode {
|
||||
if case .video = state.cameraState.mode, isTablet {
|
||||
isVideoRecording = true
|
||||
} else if state.cameraState.recording != .none {
|
||||
isVideoRecording = true
|
||||
@ -607,6 +640,13 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let timePosition: CGPoint
|
||||
if isTablet {
|
||||
timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0)
|
||||
} else {
|
||||
timePosition = CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)
|
||||
}
|
||||
|
||||
if state.cameraState.recording != .none {
|
||||
let timeBackground = timeBackground.update(
|
||||
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
|
||||
@ -614,19 +654,19 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(timeBackground
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
|
||||
.position(timePosition)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
context.add(timeLabel
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0))
|
||||
.position(timePosition)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
|
||||
if case .holding = state.cameraState.recording {
|
||||
if case .holding = state.cameraState.recording, !isTablet {
|
||||
let hintText: String?
|
||||
switch state.swipeHint {
|
||||
case .none:
|
||||
@ -656,8 +696,15 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
if case .none = state.cameraState.recording, !state.isTransitioning {
|
||||
let availableModeControlSize: CGSize
|
||||
if isTablet {
|
||||
availableModeControlSize = CGSize(width: panelWidth, height: 120.0)
|
||||
} else {
|
||||
availableModeControlSize = availableSize
|
||||
}
|
||||
let modeControl = modeControl.update(
|
||||
component: ModeComponent(
|
||||
isTablet: isTablet,
|
||||
availableModes: [.photo, .video],
|
||||
currentMode: state.cameraState.mode,
|
||||
updatedMode: { [weak state] mode in
|
||||
@ -667,12 +714,18 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
},
|
||||
tag: modeControlTag
|
||||
),
|
||||
availableSize: availableSize,
|
||||
availableSize: availableModeControlSize,
|
||||
transition: context.transition
|
||||
)
|
||||
let modeControlPosition: CGPoint
|
||||
if isTablet {
|
||||
modeControlPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 + modeControl.size.height + 26.0)
|
||||
} else {
|
||||
modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0)
|
||||
}
|
||||
context.add(modeControl
|
||||
.clipsToBounds(true)
|
||||
.position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0))
|
||||
.position(modeControlPosition)
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
@ -734,12 +787,30 @@ public class CameraScreen: ViewController {
|
||||
case instantVideo
|
||||
}
|
||||
|
||||
public enum PIPPosition {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomLeft
|
||||
case bottomRight
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
case pendingImage
|
||||
case image(UIImage, UIImage?)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case image(UIImage, UIImage?, CameraScreen.PIPPosition)
|
||||
case video(String, UIImage?, String?, UIImage?, PixelDimensions, CameraScreen.PIPPosition)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
|
||||
func withPIPPosition(_ position: CameraScreen.PIPPosition) -> Result {
|
||||
switch self {
|
||||
case let .image(mainImage, additionalImage, _):
|
||||
return .image(mainImage, additionalImage, position)
|
||||
case let .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, _):
|
||||
return .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, position)
|
||||
default:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class TransitionIn {
|
||||
@ -846,6 +917,10 @@ public class CameraScreen: ViewController {
|
||||
|
||||
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
|
||||
|
||||
private let flipAnimationAction = ActionSlot<Void>()
|
||||
|
||||
private var pipPosition: PIPPosition = .bottomRight
|
||||
|
||||
init(controller: CameraScreen) {
|
||||
self.controller = controller
|
||||
self.context = controller.context
|
||||
@ -994,9 +1069,13 @@ public class CameraScreen: ViewController {
|
||||
|
||||
self.completion.connect { [weak self] result in
|
||||
if let self {
|
||||
let pipPosition = self.pipPosition
|
||||
self.animateOutToEditor()
|
||||
self.controller?.completion(
|
||||
result
|
||||
|> map { result in
|
||||
return result.withPIPPosition(pipPosition)
|
||||
}
|
||||
|> beforeNext { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
@ -1070,6 +1149,9 @@ public class CameraScreen: ViewController {
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:)))
|
||||
self.additionalPreviewView?.addGestureRecognizer(pipPanGestureRecognizer)
|
||||
|
||||
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
|
||||
self.camera.startCapture()
|
||||
}
|
||||
@ -1128,7 +1210,30 @@ public class CameraScreen: ViewController {
|
||||
self.camera.focus(at: point, autoFocus: false)
|
||||
}
|
||||
|
||||
private var pipTranslation: CGPoint?
|
||||
@objc private func handlePipPan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
let translation = gestureRecognizer.translation(in: self.view)
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.pipTranslation = translation
|
||||
self.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
case .ended, .cancelled:
|
||||
self.pipTranslation = nil
|
||||
self.pipPosition = pipPositionForLocation(layout: layout, position: location, velocity: velocity)
|
||||
self.containerLayoutUpdated(layout: layout, transition: .spring(duration: 0.4))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.transitionDimView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
self.backgroundView.alpha = 1.0
|
||||
@ -1185,8 +1290,10 @@ public class CameraScreen: ViewController {
|
||||
view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
|
||||
|
||||
self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false)
|
||||
}
|
||||
@ -1288,7 +1395,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let layout = self.validLayout else {
|
||||
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1337,7 +1444,11 @@ public class CameraScreen: ViewController {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result == self.componentHost.view {
|
||||
return self.effectivePreviewView
|
||||
if let additionalPreviewView = self.additionalPreviewView, additionalPreviewView.bounds.contains(self.view.convert(point, to: additionalPreviewView)) {
|
||||
return additionalPreviewView
|
||||
} else {
|
||||
return self.effectivePreviewView
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -1379,6 +1490,19 @@ public class CameraScreen: ViewController {
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
let panelWidth: CGFloat
|
||||
let previewFrame: CGRect
|
||||
let viewfinderFrame: CGRect
|
||||
if isTablet {
|
||||
previewFrame = CGRect(origin: .zero, size: layout.size)
|
||||
viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize)
|
||||
panelWidth = viewfinderFrame.minX
|
||||
} else {
|
||||
previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
viewfinderFrame = previewFrame
|
||||
panelWidth = 0.0
|
||||
}
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
navigationHeight: 0.0,
|
||||
@ -1408,7 +1532,7 @@ public class CameraScreen: ViewController {
|
||||
self.hasAppeared = hasAppeared
|
||||
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
||||
|
||||
self.presentDualCameraTooltip()
|
||||
// self.presentDualCameraTooltip()
|
||||
}
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
@ -1419,6 +1543,8 @@ public class CameraScreen: ViewController {
|
||||
camera: self.camera,
|
||||
updateState: self.updateState,
|
||||
hasAppeared: self.hasAppeared,
|
||||
panelWidth: panelWidth,
|
||||
flipAnimationAction: self.flipAnimationAction,
|
||||
present: { [weak self] c in
|
||||
self?.controller?.present(c, in: .window(.root))
|
||||
},
|
||||
@ -1452,16 +1578,6 @@ public class CameraScreen: ViewController {
|
||||
|
||||
transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
let previewFrame: CGRect
|
||||
let viewfinderFrame: CGRect
|
||||
if isTablet {
|
||||
previewFrame = CGRect(origin: .zero, size: layout.size)
|
||||
viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize)
|
||||
} else {
|
||||
previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
viewfinderFrame = previewFrame
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
self.currentPreviewView.layer.cornerRadius = 0.0
|
||||
transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
@ -1470,7 +1586,36 @@ public class CameraScreen: ViewController {
|
||||
|
||||
if let additionalPreviewView = self.currentAdditionalPreviewView {
|
||||
additionalPreviewView.layer.cornerRadius = 80.0
|
||||
let additionalPreviewFrame = CGRect(origin: CGPoint(x: previewFrame.width - 160.0 - 10.0 + (self.isDualCamEnabled ? 0.0 : 180.0), y: previewFrame.height - 160.0 - 81.0), size: CGSize(width: 160.0, height: 160.0))
|
||||
|
||||
var origin: CGPoint
|
||||
switch self.pipPosition {
|
||||
case .topLeft:
|
||||
origin = CGPoint(x: 10.0, y: 110.0)
|
||||
if !self.isDualCamEnabled {
|
||||
origin = origin.offsetBy(dx: -180.0, dy: 0.0)
|
||||
}
|
||||
case .topRight:
|
||||
origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: 110.0)
|
||||
if !self.isDualCamEnabled {
|
||||
origin = origin.offsetBy(dx: 180.0, dy: 0.0)
|
||||
}
|
||||
case .bottomLeft:
|
||||
origin = CGPoint(x: 10.0, y: previewFrame.height - 160.0 - 110.0)
|
||||
if !self.isDualCamEnabled {
|
||||
origin = origin.offsetBy(dx: -180.0, dy: 0.0)
|
||||
}
|
||||
case .bottomRight:
|
||||
origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: previewFrame.height - 160.0 - 110.0)
|
||||
if !self.isDualCamEnabled {
|
||||
origin = origin.offsetBy(dx: 180.0, dy: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
if let pipTranslation = self.pipTranslation {
|
||||
origin = origin.offsetBy(dx: pipTranslation.x, dy: pipTranslation.y)
|
||||
}
|
||||
|
||||
let additionalPreviewFrame = CGRect(origin: origin, size: CGSize(width: 160.0, height: 160.0))
|
||||
transition.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center)
|
||||
transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size))
|
||||
|
||||
@ -1500,6 +1645,10 @@ public class CameraScreen: ViewController {
|
||||
|
||||
transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height)))
|
||||
|
||||
if isTablet && isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1563,6 +1712,10 @@ public class CameraScreen: ViewController {
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.requestAudioSession()
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
@ -1571,6 +1724,9 @@ public class CameraScreen: ViewController {
|
||||
|
||||
deinit {
|
||||
self.audioSessionDisposable?.dispose()
|
||||
if #available(iOS 13.0, *) {
|
||||
try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(false)
|
||||
}
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
@ -1611,7 +1767,17 @@ public class CameraScreen: ViewController {
|
||||
if let current = self.galleryController {
|
||||
controller = current
|
||||
} else {
|
||||
controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
|
||||
controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, getSourceRect: { [weak self] in
|
||||
if let self {
|
||||
if let galleryButton = self.node.componentHost.findTaggedView(tag: galleryButtonTag) {
|
||||
return galleryButton.convert(galleryButton.bounds, to: self.view).offsetBy(dx: 0.0, dy: -15.0)
|
||||
} else {
|
||||
return .zero
|
||||
}
|
||||
} else {
|
||||
return .zero
|
||||
}
|
||||
}, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in
|
||||
if let self {
|
||||
stopCameraCapture()
|
||||
|
||||
@ -1665,15 +1831,21 @@ public class CameraScreen: ViewController {
|
||||
self.node.camera.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
if animated {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
if !interactive {
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
if !interactive {
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
}
|
||||
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
@ -1694,6 +1866,9 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
return
|
||||
}
|
||||
let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0)
|
||||
transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
@ -1713,6 +1888,9 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
|
||||
return
|
||||
}
|
||||
if dismissing {
|
||||
if transitionFraction < 0.7 || velocity < -1000.0 {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
@ -1842,3 +2020,109 @@ private final class DualIconComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoint, velocity: CGPoint) -> CameraScreen.PIPPosition {
|
||||
var layoutInsets = layout.insets(options: [.input])
|
||||
layoutInsets.bottom += 48.0
|
||||
var result = CGPoint()
|
||||
if position.x < layout.size.width / 2.0 {
|
||||
result.x = 0.0
|
||||
} else {
|
||||
result.x = 1.0
|
||||
}
|
||||
if position.y < layoutInsets.top + (layout.size.height - layoutInsets.bottom - layoutInsets.top) / 2.0 {
|
||||
result.y = 0.0
|
||||
} else {
|
||||
result.y = 1.0
|
||||
}
|
||||
|
||||
let currentPosition = result
|
||||
|
||||
let angleEpsilon: CGFloat = 30.0
|
||||
var shouldHide = false
|
||||
|
||||
if (velocity.x * velocity.x + velocity.y * velocity.y) >= 500.0 * 500.0 {
|
||||
let x = velocity.x
|
||||
let y = velocity.y
|
||||
|
||||
var angle = atan2(y, x) * 180.0 / CGFloat.pi * -1.0
|
||||
if angle < 0.0 {
|
||||
angle += 360.0
|
||||
}
|
||||
|
||||
if currentPosition.x.isZero && currentPosition.y.isZero {
|
||||
if ((angle > 0 && angle < 90 - angleEpsilon) || angle > 360 - angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 0.0
|
||||
} else if (angle > 180 + angleEpsilon && angle < 270 + angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 1.0
|
||||
} else if (angle > 270 + angleEpsilon && angle < 360 - angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 1.0
|
||||
} else {
|
||||
shouldHide = true
|
||||
}
|
||||
} else if !currentPosition.x.isZero && currentPosition.y.isZero {
|
||||
if (angle > 90 + angleEpsilon && angle < 180 + angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 0.0
|
||||
}
|
||||
else if (angle > 270 - angleEpsilon && angle < 360 - angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 1.0
|
||||
}
|
||||
else if (angle > 180 + angleEpsilon && angle < 270 - angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 1.0
|
||||
}
|
||||
else {
|
||||
shouldHide = true
|
||||
}
|
||||
} else if currentPosition.x.isZero && !currentPosition.y.isZero {
|
||||
if (angle > 90 - angleEpsilon && angle < 180 - angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 0.0
|
||||
}
|
||||
else if (angle < angleEpsilon || angle > 270 + angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 1.0
|
||||
}
|
||||
else if (angle > angleEpsilon && angle < 90 - angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 0.0
|
||||
}
|
||||
else if (!shouldHide) {
|
||||
shouldHide = true
|
||||
}
|
||||
} else if !currentPosition.x.isZero && !currentPosition.y.isZero {
|
||||
if (angle > angleEpsilon && angle < 90 + angleEpsilon) {
|
||||
result.x = 1.0
|
||||
result.y = 0.0
|
||||
}
|
||||
else if (angle > 180 - angleEpsilon && angle < 270 - angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 1.0
|
||||
}
|
||||
else if (angle > 90 + angleEpsilon && angle < 180 - angleEpsilon) {
|
||||
result.x = 0.0
|
||||
result.y = 0.0
|
||||
}
|
||||
else if (!shouldHide) {
|
||||
shouldHide = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var position: CameraScreen.PIPPosition = .bottomRight
|
||||
if result.x == 0.0 && result.y == 0.0 {
|
||||
position = .topLeft
|
||||
} else if result.x == 1.0 && result.y == 0.0 {
|
||||
position = .topRight
|
||||
} else if result.x == 0.0 && result.y == 1.0 {
|
||||
position = .bottomLeft
|
||||
} else if result.x == 1.0 && result.y == 1.0 {
|
||||
position = .bottomRight
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
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)
|
||||
|
||||
final class ModeComponent: Component {
|
||||
let isTablet: Bool
|
||||
let availableModes: [CameraMode]
|
||||
let currentMode: CameraMode
|
||||
let updatedMode: (CameraMode) -> Void
|
||||
let tag: AnyObject?
|
||||
|
||||
init(
|
||||
isTablet: Bool,
|
||||
availableModes: [CameraMode],
|
||||
currentMode: CameraMode,
|
||||
updatedMode: @escaping (CameraMode) -> Void,
|
||||
tag: AnyObject?
|
||||
) {
|
||||
self.isTablet = isTablet
|
||||
self.availableModes = availableModes
|
||||
self.currentMode = currentMode
|
||||
self.updatedMode = updatedMode
|
||||
@ -36,6 +39,9 @@ final class ModeComponent: Component {
|
||||
}
|
||||
|
||||
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
|
||||
if lhs.isTablet != rhs.isTablet {
|
||||
return false
|
||||
}
|
||||
if lhs.availableModes != rhs.availableModes {
|
||||
return false
|
||||
}
|
||||
@ -114,14 +120,16 @@ final class ModeComponent: Component {
|
||||
|
||||
func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
|
||||
let isTablet = component.isTablet
|
||||
let updatedMode = component.updatedMode
|
||||
|
||||
let spacing: CGFloat = 14.0
|
||||
|
||||
let spacing: CGFloat = isTablet ? 9.0 : 14.0
|
||||
|
||||
var i = 0
|
||||
var itemFrame = CGRect(origin: .zero, size: buttonSize)
|
||||
var selectedCenter = itemFrame.minX
|
||||
|
||||
for mode in component.availableModes {
|
||||
let itemView: ItemView
|
||||
if self.itemViews.count == i {
|
||||
@ -137,20 +145,37 @@ final class ModeComponent: Component {
|
||||
|
||||
itemView.update(value: mode.title, selected: mode == component.currentMode)
|
||||
itemView.bounds = CGRect(origin: .zero, size: itemFrame.size)
|
||||
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
|
||||
if mode == component.currentMode {
|
||||
selectedCenter = itemFrame.midX
|
||||
if isTablet {
|
||||
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
|
||||
if mode == component.currentMode {
|
||||
selectedCenter = itemFrame.midY
|
||||
}
|
||||
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: buttonSize.height + spacing)
|
||||
} else {
|
||||
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
if mode == component.currentMode {
|
||||
selectedCenter = itemFrame.midX
|
||||
}
|
||||
itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0)
|
||||
}
|
||||
|
||||
|
||||
i += 1
|
||||
itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0)
|
||||
}
|
||||
|
||||
let totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height)
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize))
|
||||
let totalSize: CGSize
|
||||
let size: CGSize
|
||||
if isTablet {
|
||||
totalSize = CGSize(width: availableSize.width, height: buttonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
|
||||
size = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize))
|
||||
} else {
|
||||
size = CGSize(width: availableSize.width, height: buttonSize.height)
|
||||
totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height)
|
||||
transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: buttonSize.height)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,12 +121,6 @@ private func lookupSpringValue(_ t: CGFloat) -> CGFloat {
|
||||
}
|
||||
}
|
||||
return 1.0
|
||||
// print("---start---")
|
||||
// for i in 0 ..< 16 {
|
||||
// let j = Double(i) * 1.0 / 16.0
|
||||
// print("\(j) \(listViewAnimationCurveSystem(j))")
|
||||
// }
|
||||
// print("---end---")
|
||||
}
|
||||
|
||||
private class ShutterBlobLayer: MetalImageLayer {
|
||||
@ -214,12 +208,14 @@ final class ShutterBlobView: UIView {
|
||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
private var primarySize = AnimatableProperty<CGFloat>(value: 0.63)
|
||||
private var primaryOffset = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var primaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var primaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var primaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var primaryCornerRadius = AnimatableProperty<CGFloat>(value: 0.63)
|
||||
|
||||
private var secondarySize = AnimatableProperty<CGFloat>(value: 0.34)
|
||||
private var secondaryOffset = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var secondaryOffsetX = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var secondaryOffsetY = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
private var secondaryRedness = AnimatableProperty<CGFloat>(value: 0.0)
|
||||
|
||||
private(set) var state: BlobState = .generic
|
||||
@ -309,22 +305,42 @@ final class ShutterBlobView: UIView {
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func updatePrimaryOffset(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
func updatePrimaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.primaryOffset.update(value: mappedOffset, transition: transition)
|
||||
self.primaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func updateSecondaryOffset(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
func updatePrimaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.frame.width * 2.0
|
||||
self.primaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func updateSecondaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.secondaryOffset.update(value: mappedOffset, transition: transition)
|
||||
self.secondaryOffsetX.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func updateSecondaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) {
|
||||
guard self.frame.height > 0.0 else {
|
||||
return
|
||||
}
|
||||
let mappedOffset = offset / self.frame.width * 2.0
|
||||
self.secondaryOffsetY.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
@ -332,11 +348,13 @@ final class ShutterBlobView: UIView {
|
||||
private func updateAnimations() {
|
||||
let properties = [
|
||||
self.primarySize,
|
||||
self.primaryOffset,
|
||||
self.primaryOffsetX,
|
||||
self.primaryOffsetY,
|
||||
self.primaryRedness,
|
||||
self.primaryCornerRadius,
|
||||
self.secondarySize,
|
||||
self.secondaryOffset,
|
||||
self.secondaryOffsetX,
|
||||
self.secondaryOffsetY,
|
||||
self.secondaryRedness
|
||||
]
|
||||
|
||||
@ -407,20 +425,31 @@ final class ShutterBlobView: UIView {
|
||||
var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height))
|
||||
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
var primaryParameters = simd_float4(
|
||||
var primaryParameters = simd_float3(
|
||||
Float(self.primarySize.presentationValue),
|
||||
Float(self.primaryOffset.presentationValue),
|
||||
Float(self.primaryRedness.presentationValue),
|
||||
Float(self.primaryCornerRadius.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float4>.size, index: 1)
|
||||
renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout<simd_float3>.size, index: 1)
|
||||
|
||||
var secondaryParameters = simd_float3(
|
||||
var primaryOffset = simd_float2(
|
||||
Float(self.primaryOffsetX.presentationValue),
|
||||
Float(self.primaryOffsetY.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&primaryOffset, length: MemoryLayout<simd_float2>.size, index: 2)
|
||||
|
||||
var secondaryParameters = simd_float2(
|
||||
Float(self.secondarySize.presentationValue),
|
||||
Float(self.secondaryOffset.presentationValue),
|
||||
Float(self.secondaryRedness.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float3>.size, index: 2)
|
||||
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float4>.size, index: 3)
|
||||
|
||||
var secondaryOffset = simd_float2(
|
||||
Float(self.secondaryOffsetX.presentationValue),
|
||||
Float(self.secondaryOffsetY.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout<simd_float2>.size, index: 4)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
|
@ -8,6 +8,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
public enum Content: Equatable {
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
case video(String, UIImage?)
|
||||
|
||||
public static func == (lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
@ -23,6 +24,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .video(lhsPath, _):
|
||||
if case let .video(rhsPath, _) = rhs {
|
||||
return lhsPath == rhsPath
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,6 +37,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case uuid
|
||||
case file
|
||||
case image
|
||||
case videoPath
|
||||
case videoImage
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
case scale
|
||||
@ -64,6 +73,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm"
|
||||
case .image:
|
||||
return false
|
||||
case .video:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +103,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
self.content = .file(file)
|
||||
} else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
||||
self.content = .image(image)
|
||||
} else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath), let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) {
|
||||
self.content = .video(videoPath, image)
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
@ -110,6 +123,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
try container.encode(file, forKey: .file)
|
||||
case let .image(image):
|
||||
try container.encodeIfPresent(image.pngData(), forKey: .image)
|
||||
case let .video(path, image):
|
||||
try container.encode(path, forKey: .videoPath)
|
||||
try container.encodeIfPresent(image?.jpegData(compressionQuality: 0.87), forKey: .videoImage)
|
||||
}
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
|
@ -420,11 +420,16 @@ public final class MediaEditor {
|
||||
if let self {
|
||||
let start = self.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000)))
|
||||
self.onPlaybackAction(.seek(start))
|
||||
self.player?.play()
|
||||
self.onPlaybackAction(.play)
|
||||
}
|
||||
})
|
||||
player.playImmediately(atRate: 1.0)
|
||||
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
|
||||
Queue.mainQueue().justDispatch {
|
||||
player.playImmediately(atRate: 1.0)
|
||||
self.onPlaybackAction(.play)
|
||||
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -460,6 +465,12 @@ public final class MediaEditor {
|
||||
return self.values.toolValues[key]
|
||||
}
|
||||
|
||||
private var previewUnedited = false
|
||||
public func setPreviewUnedited(_ preview: Bool) {
|
||||
self.previewUnedited = preview
|
||||
self.updateRenderChain()
|
||||
}
|
||||
|
||||
public func setToolValue(_ key: EditorToolKey, value: Any) {
|
||||
self.updateValues { values in
|
||||
var updatedToolValues = values.toolValues
|
||||
@ -481,11 +492,20 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlaybackAction {
|
||||
case play
|
||||
case pause
|
||||
case seek(Double)
|
||||
}
|
||||
|
||||
public var onPlaybackAction: (PlaybackAction) -> Void = { _ in }
|
||||
|
||||
private var targetTimePosition: (CMTime, Bool)?
|
||||
private var updatingTimePosition = false
|
||||
public func seek(_ position: Double, andPlay play: Bool) {
|
||||
if !play {
|
||||
self.player?.pause()
|
||||
self.onPlaybackAction(.pause)
|
||||
}
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
|
||||
if self.targetTimePosition?.0 != targetPosition {
|
||||
@ -496,6 +516,7 @@ public final class MediaEditor {
|
||||
}
|
||||
if play {
|
||||
self.player?.play()
|
||||
self.onPlaybackAction(.play)
|
||||
}
|
||||
}
|
||||
|
||||
@ -505,14 +526,17 @@ public final class MediaEditor {
|
||||
|
||||
public func play() {
|
||||
self.player?.play()
|
||||
self.onPlaybackAction(.play)
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
self.player?.pause()
|
||||
self.onPlaybackAction(.pause)
|
||||
}
|
||||
|
||||
public func invalidate() {
|
||||
self.player?.pause()
|
||||
self.onPlaybackAction(.pause)
|
||||
self.renderer.textureSource?.invalidate()
|
||||
}
|
||||
|
||||
@ -531,6 +555,7 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
})
|
||||
self.onPlaybackAction(.seek(targetPosition.seconds))
|
||||
}
|
||||
|
||||
public func setVideoTrimRange(_ trimRange: Range<Double>, apply: Bool) {
|
||||
@ -558,6 +583,7 @@ public final class MediaEditor {
|
||||
private var previousUpdateTime: Double?
|
||||
private var scheduledUpdate = false
|
||||
private func updateRenderChain() {
|
||||
self.renderer.renderPassedEnabled = !self.previewUnedited
|
||||
self.renderChain.update(values: self.values)
|
||||
if let player = self.player, player.rate > 0.0 {
|
||||
} else {
|
||||
|
@ -20,6 +20,8 @@ func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, c
|
||||
content = .file(file)
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
case let .video(path, _):
|
||||
content = .video(path)
|
||||
}
|
||||
return [MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)]
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
|
||||
@ -69,6 +71,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
public enum Content {
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
case video(String)
|
||||
|
||||
var file: TelegramMediaFile? {
|
||||
if case let .file(file) = self {
|
||||
@ -90,7 +93,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
var source: AnimatedStickerNodeSource?
|
||||
var frameSource = Promise<QueueLocalObject<AnimatedStickerDirectFrameSource>?>()
|
||||
var videoFrameSource = Promise<QueueLocalObject<VideoStickerDirectFrameSource>?>()
|
||||
var isVideo = false
|
||||
var isVideoSticker = false
|
||||
|
||||
var assetReader: AVAssetReader?
|
||||
var videoOutput: AVAssetReaderTrackOutput?
|
||||
|
||||
var frameCount: Int?
|
||||
var frameRate: Int?
|
||||
@ -118,9 +124,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
case let .file(file):
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
self.isAnimated = true
|
||||
self.isVideo = file.isVideoSticker || file.mimeType == "video/webm"
|
||||
self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm"
|
||||
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideo)
|
||||
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker)
|
||||
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
if let source = self.source {
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
@ -131,7 +137,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
let queue = strongSelf.queue
|
||||
|
||||
if strongSelf.isVideo {
|
||||
if strongSelf.isVideoSticker {
|
||||
let frameSource = QueueLocalObject<VideoStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return VideoStickerDirectFrameSource(queue: queue, path: path, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, unpremultiplyAlpha: false)!
|
||||
})
|
||||
@ -180,6 +186,27 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
case let .image(image):
|
||||
self.isAnimated = false
|
||||
self.imagePromise.set(.single(image))
|
||||
case let .video(videoPath):
|
||||
self.isAnimated = true
|
||||
|
||||
let url = URL(fileURLWithPath: videoPath)
|
||||
let asset = AVURLAsset(url: url)
|
||||
|
||||
if let assetReader = try? AVAssetReader(asset: asset), let videoTrack = asset.tracks(withMediaType: .video).first {
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true
|
||||
]
|
||||
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings)
|
||||
videoOutput.alwaysCopiesSampleData = true
|
||||
if assetReader.canAdd(videoOutput) {
|
||||
assetReader.add(videoOutput)
|
||||
}
|
||||
|
||||
assetReader.startReading()
|
||||
self.assetReader = assetReader
|
||||
self.videoOutput = videoOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,9 +214,52 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
self.disposables.dispose()
|
||||
}
|
||||
|
||||
var tested = false
|
||||
private var circleMaskFilter: CIFilter?
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) {
|
||||
if self.isAnimated {
|
||||
if case .video = self.content {
|
||||
if let videoOutput = self.videoOutput {
|
||||
if let sampleBuffer = videoOutput.copyNextSampleBuffer(), let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
|
||||
var ciImage = CIImage(cvPixelBuffer: imageBuffer)
|
||||
ciImage = ciImage.oriented(forExifOrientation: UIImage.Orientation.right.exifOrientation)
|
||||
let minSide = min(ciImage.extent.size.width, ciImage.extent.size.height)
|
||||
let cropRect = CGRect(origin: CGPoint(x: floor((ciImage.extent.size.width - minSide) / 2.0), y: floor((ciImage.extent.size.height - minSide) / 2.0)), size: CGSize(width: minSide, height: minSide))
|
||||
ciImage = ciImage.cropped(to: cropRect).samplingLinear()
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: -ciImage.extent.midX, y: -ciImage.extent.midY))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(rotationAngle: -.pi / 2.0))
|
||||
// ciImage = ciImage.transformed(by: CGAffineTransform(translationX: ciImage.extent.midX, y: ciImage.extent.midY))
|
||||
|
||||
var circleMaskFilter: CIFilter?
|
||||
if let current = self.circleMaskFilter {
|
||||
circleMaskFilter = current
|
||||
} else {
|
||||
let circleImage = generateImage(CGSize(width: minSide, height: minSide), scale: 1.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: .zero, size: size))
|
||||
})!
|
||||
let circleMask = CIImage(image: circleImage)
|
||||
if let filter = CIFilter(name: "CIBlendWithAlphaMask") {
|
||||
filter.setValue(circleMask, forKey: kCIInputMaskImageKey)
|
||||
self.circleMaskFilter = filter
|
||||
circleMaskFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
let _ = circleMaskFilter
|
||||
if let circleMaskFilter {
|
||||
circleMaskFilter.setValue(ciImage, forKey: kCIInputImageKey)
|
||||
if let output = circleMaskFilter.outputImage {
|
||||
ciImage = output
|
||||
}
|
||||
}
|
||||
|
||||
completion(ciImage)
|
||||
}
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
} else if self.isAnimated {
|
||||
let currentTime = CMTimeGetSeconds(time)
|
||||
|
||||
var tintColor: UIColor?
|
||||
@ -262,7 +332,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
if self.isVideo {
|
||||
if self.isVideoSticker {
|
||||
self.disposables.add((self.videoFrameSource.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] frameSource in
|
||||
@ -371,3 +441,20 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type:
|
||||
|
||||
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace])
|
||||
}
|
||||
|
||||
private extension UIImage.Orientation {
|
||||
var exifOrientation: Int32 {
|
||||
switch self {
|
||||
case .up: return 1
|
||||
case .down: return 3
|
||||
case .left: return 8
|
||||
case .right: return 6
|
||||
case .upMirrored: return 2
|
||||
case .downMirrored: return 4
|
||||
case .leftMirrored: return 5
|
||||
case .rightMirrored: return 7
|
||||
@unknown default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +150,8 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
}
|
||||
|
||||
var renderPassedEnabled = true
|
||||
|
||||
func renderFrame() {
|
||||
let device: MTLDevice?
|
||||
if let renderTarget = self.renderTarget {
|
||||
@ -181,9 +183,11 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
for renderPass in self.renderPasses {
|
||||
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
|
||||
texture = nextTexture
|
||||
if self.renderPassedEnabled {
|
||||
for renderPass in self.renderPasses {
|
||||
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
|
||||
texture = nextTexture
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finalTexture = texture
|
||||
|
@ -37,6 +37,7 @@ swift_library(
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/CameraButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//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 UIKit
|
||||
import CoreServices
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
@ -23,6 +24,7 @@ import ShareWithPeersScreen
|
||||
import PresentationDataUtils
|
||||
import ContextUI
|
||||
import BundleIconComponent
|
||||
import CameraButtonComponent
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -41,6 +43,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
let isDisplayingTool: Bool
|
||||
let isInteractingWithEntities: Bool
|
||||
let isSavingAvailable: Bool
|
||||
let hasAppeared: Bool
|
||||
let isDismissing: Bool
|
||||
let mediaEditor: MediaEditor?
|
||||
let privacy: MediaEditorResultPrivacy
|
||||
@ -54,6 +57,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
isDisplayingTool: Bool,
|
||||
isInteractingWithEntities: Bool,
|
||||
isSavingAvailable: Bool,
|
||||
hasAppeared: Bool,
|
||||
isDismissing: Bool,
|
||||
mediaEditor: MediaEditor?,
|
||||
privacy: MediaEditorResultPrivacy,
|
||||
@ -66,6 +70,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
self.isDisplayingTool = isDisplayingTool
|
||||
self.isInteractingWithEntities = isInteractingWithEntities
|
||||
self.isSavingAvailable = isSavingAvailable
|
||||
self.hasAppeared = hasAppeared
|
||||
self.isDismissing = isDismissing
|
||||
self.mediaEditor = mediaEditor
|
||||
self.privacy = privacy
|
||||
@ -88,6 +93,9 @@ final class MediaEditorScreenComponent: Component {
|
||||
if lhs.isSavingAvailable != rhs.isSavingAvailable {
|
||||
return false
|
||||
}
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
if lhs.isDismissing != rhs.isDismissing {
|
||||
return false
|
||||
}
|
||||
@ -175,6 +183,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
deinit {
|
||||
self.playerStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
var muteDidChange = false
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
@ -239,7 +249,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
case camera
|
||||
case gallery
|
||||
}
|
||||
func animateIn(from source: TransitionAnimationSource) {
|
||||
func animateIn(from source: TransitionAnimationSource, completion: @escaping () -> Void = {}) {
|
||||
let buttons = [
|
||||
self.drawButton,
|
||||
self.textButton,
|
||||
@ -269,6 +279,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0)
|
||||
})
|
||||
delay += 0.03
|
||||
|
||||
Queue.mainQueue().after(0.45, completion)
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,11 +464,28 @@ final class MediaEditorScreenComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
let openDrawing = component.openDrawing
|
||||
let openTools = component.openTools
|
||||
|
||||
let buttonSideInset: CGFloat = 10.0
|
||||
let buttonSideInset: CGFloat
|
||||
let buttonBottomInset: CGFloat = 8.0
|
||||
let previewSize: CGSize
|
||||
let topInset: CGFloat = environment.statusBarHeight + 12.0
|
||||
if isTablet {
|
||||
let previewHeight = availableSize.height - topInset - 75.0
|
||||
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
|
||||
buttonSideInset = 30.0
|
||||
} else {
|
||||
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
|
||||
buttonSideInset = 10.0
|
||||
}
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
@ -534,6 +563,16 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setAlpha(view: doneButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let buttonsAvailableWidth: CGFloat
|
||||
let buttonsLeftOffset: CGFloat
|
||||
if isTablet {
|
||||
buttonsAvailableWidth = previewSize.width + 260.0
|
||||
buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0)
|
||||
} else {
|
||||
buttonsAvailableWidth = availableSize.width
|
||||
buttonsLeftOffset = 0.0
|
||||
}
|
||||
|
||||
let drawButtonSize = self.drawButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
@ -549,7 +588,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let drawButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
size: drawButtonSize
|
||||
)
|
||||
if let drawButtonView = self.drawButton.view {
|
||||
@ -576,7 +615,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let textButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
size: textButtonSize
|
||||
)
|
||||
if let textButtonView = self.textButton.view {
|
||||
@ -603,7 +642,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let stickerButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
size: stickerButtonSize
|
||||
)
|
||||
if let stickerButtonView = self.stickerButton.view {
|
||||
@ -630,7 +669,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let toolsButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
size: toolsButtonSize
|
||||
)
|
||||
if let toolsButtonView = self.toolsButton.view {
|
||||
@ -719,6 +758,14 @@ final class MediaEditorScreenComponent: Component {
|
||||
timeoutSelected = timeout != nil
|
||||
}
|
||||
|
||||
|
||||
var inputPanelAvailableWidth = previewSize.width
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) {
|
||||
inputPanelAvailableWidth += 200.0
|
||||
}
|
||||
}
|
||||
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
@ -765,7 +812,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
bottomInset: 0.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 200.0)
|
||||
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)
|
||||
)
|
||||
|
||||
let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut))
|
||||
@ -802,7 +849,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom
|
||||
inputPanelOffset = inputPanelBottomInset
|
||||
}
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize)
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize)
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
if inputPanelView.superview == nil {
|
||||
self.addSubview(inputPanelView)
|
||||
@ -839,6 +886,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(
|
||||
PrivacyButtonComponent(
|
||||
backgroundColor: isTablet ? UIColor(rgb: 0x303030, alpha: 0.5) : UIColor(white: 0.0, alpha: 0.5),
|
||||
icon: UIImage(bundleImageName: "Media Editor/Recipient")!,
|
||||
text: privacyText
|
||||
)
|
||||
@ -852,10 +900,18 @@ final class MediaEditorScreenComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let privacyButtonFrame = CGRect(
|
||||
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
size: privacyButtonSize
|
||||
)
|
||||
let privacyButtonFrame: CGRect
|
||||
if isTablet {
|
||||
privacyButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width - privacyButtonSize.width - 24.0, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0),
|
||||
size: privacyButtonSize
|
||||
)
|
||||
} else {
|
||||
privacyButtonFrame = CGRect(
|
||||
origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
size: privacyButtonSize
|
||||
)
|
||||
}
|
||||
if let privacyButtonView = self.privacyButton.view {
|
||||
if privacyButtonView.superview == nil {
|
||||
self.addSubview(privacyButtonView)
|
||||
@ -866,10 +922,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let saveButtonSize = self.saveButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(
|
||||
let saveContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
saveContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: "anim_storysave",
|
||||
@ -877,9 +934,26 @@ final class MediaEditorScreenComponent: Component {
|
||||
range: nil
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 33.0, height: 33.0)
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
).tagged(saveButtonTag)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
saveContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Editor/SaveIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let saveButtonSize = self.saveButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(CameraButton(
|
||||
content: saveContentComponent,
|
||||
action: { [weak self] in
|
||||
if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View {
|
||||
view.playOnce()
|
||||
@ -916,22 +990,42 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
if let playerState = state.playerState, playerState.hasAudio {
|
||||
let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false
|
||||
let muteButtonSize = self.muteButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(
|
||||
|
||||
let muteContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
muteContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: "anim_storymute",
|
||||
mode: .animating(loop: false),
|
||||
mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin),
|
||||
range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0)
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 33.0, height: 33.0)
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
).tagged(muteButtonTag)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
muteContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Editor/MuteIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let muteButtonSize = self.muteButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(CameraButton(
|
||||
content: muteContentComponent,
|
||||
action: { [weak self, weak state] in
|
||||
if let self, let mediaEditor = self.component?.mediaEditor {
|
||||
state?.muteDidChange = true
|
||||
let isMuted = !mediaEditor.values.videoIsMuted
|
||||
mediaEditor.setVideoIsMuted(isMuted)
|
||||
state?.updated()
|
||||
@ -1106,7 +1200,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
private let storyMaxVideoDuration: Double = 60.0
|
||||
|
||||
public final class MediaEditorScreen: ViewController {
|
||||
public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate {
|
||||
public enum TransitionIn {
|
||||
public final class GalleryTransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
@ -1166,6 +1260,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
private var wasPlaying = false
|
||||
|
||||
private let backgroundDimView: UIView
|
||||
fileprivate let containerView: UIView
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
fileprivate let storyPreview: ComponentView<Empty>
|
||||
fileprivate let toolValue: ComponentView<Empty>
|
||||
@ -1190,6 +1285,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
private var isDisplayingTool = false
|
||||
private var isInteractingWithEntities = false
|
||||
private var isEnhancing = false
|
||||
|
||||
private var hasAppeared = false
|
||||
private var isDismissing = false
|
||||
private var dismissOffset: CGFloat = 0.0
|
||||
private var isDismissed = false
|
||||
@ -1207,6 +1304,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.backgroundDimView.isHidden = true
|
||||
self.backgroundDimView.backgroundColor = .black
|
||||
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
|
||||
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
|
||||
self.storyPreview = ComponentView<Empty>()
|
||||
self.toolValue = ComponentView<Empty>()
|
||||
@ -1241,7 +1341,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.view.addSubview(self.backgroundDimView)
|
||||
self.view.addSubview(self.previewContainerView)
|
||||
self.view.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.previewContainerView)
|
||||
self.previewContainerView.addSubview(self.gradientView)
|
||||
self.previewContainerView.addSubview(self.entitiesContainerView)
|
||||
self.entitiesContainerView.addSubview(self.entitiesView)
|
||||
@ -1276,7 +1377,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
areUnicodeEmojiEnabled: true,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: controller.context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
|
||||
@ -1287,7 +1388,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: controller.context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasSearch: true,
|
||||
hasTrending: true,
|
||||
forceHasPremium: true
|
||||
)
|
||||
@ -1370,8 +1471,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
mediaEntity.scale = storyDimensions.width / fittedSize.width
|
||||
}
|
||||
self.entitiesView.add(mediaEntity, announce: false)
|
||||
|
||||
if case let .image(_, _, additionalImage) = subject, let additionalImage {
|
||||
|
||||
if case let .image(_, _, additionalImage, position) = subject, let additionalImage {
|
||||
let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
@ -1386,8 +1487,15 @@ public final class MediaEditorScreen: ViewController {
|
||||
imageEntity.referenceDrawingSize = storyDimensions
|
||||
imageEntity.scale = 1.49
|
||||
imageEntity.mirrored = true
|
||||
imageEntity.position = CGPoint(x: storyDimensions.width - 224.0, y: storyDimensions.height - 403.0)
|
||||
imageEntity.position = position.getPosition(storyDimensions)
|
||||
self.entitiesView.add(imageEntity, announce: false)
|
||||
} else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, position) = subject, let additionalVideoPath {
|
||||
let videoEntity = DrawingStickerEntity(content: .video(additionalVideoPath, additionalVideoImage))
|
||||
videoEntity.referenceDrawingSize = storyDimensions
|
||||
videoEntity.scale = 1.49
|
||||
videoEntity.mirrored = true
|
||||
videoEntity.position = position.getPosition(storyDimensions)
|
||||
self.entitiesView.add(videoEntity, announce: false)
|
||||
}
|
||||
|
||||
let initialPosition = mediaEntity.position
|
||||
@ -1451,6 +1559,31 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
})
|
||||
self.mediaEditor = mediaEditor
|
||||
|
||||
mediaEditor.onPlaybackAction = { [weak self] action in
|
||||
if let self {
|
||||
switch action {
|
||||
case .play:
|
||||
self.entitiesView.eachView({ view in
|
||||
if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content {
|
||||
view.play()
|
||||
}
|
||||
})
|
||||
case .pause:
|
||||
self.entitiesView.eachView({ view in
|
||||
if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content {
|
||||
view.pause()
|
||||
}
|
||||
})
|
||||
case let .seek(timestamp):
|
||||
self.entitiesView.eachView({ view in
|
||||
if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content {
|
||||
view.seek(to: timestamp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@ -1679,13 +1812,19 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
let completion: () -> Void = { [weak self] in
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self?.requestUpdate(hasAppeared: true, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
if let transitionIn = self.controller?.transitionIn {
|
||||
switch transitionIn {
|
||||
case .camera:
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera)
|
||||
view.animateIn(from: .camera, completion: completion)
|
||||
}
|
||||
if let subject = self.subject, case let .video(_, transitionImage, _) = subject, let transitionImage {
|
||||
if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _) = subject, let transitionImage {
|
||||
self.setupTransitionImage(transitionImage)
|
||||
}
|
||||
case let .gallery(transitionIn):
|
||||
@ -1703,7 +1842,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
let duration: Double = 0.4
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
@ -1719,7 +1860,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
} else {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera)
|
||||
view.animateIn(from: .camera, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1778,19 +1919,12 @@ public final class MediaEditorScreen: ViewController {
|
||||
snapshotView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
|
||||
let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width
|
||||
snapshotView.center = CGPoint(x: 0.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
|
||||
let snapshotTransform = CATransform3DMakeScale(0.001, snapshotScale, 1.0)
|
||||
//snapshotTransform.m34 = 1.0 / -500
|
||||
//snapshotTransform = CATransform3DRotate(snapshotTransform, -90.0 * .pi / 180.0, 0.0, 1.0, 0.0)
|
||||
snapshotView.layer.transform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
|
||||
|
||||
let targetTransform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0)
|
||||
//snapshotTransform
|
||||
//targetTransform = CATransform3DRotate(targetTransform, 0.0, 0.0, 1.0, 0.0)
|
||||
|
||||
snapshotView.layer.transform = snapshotTransform
|
||||
snapshotView.alpha = 0.0
|
||||
Queue.mainQueue().after(0.15) {
|
||||
snapshotView.layer.transform = targetTransform
|
||||
snapshotView.layer.animate(from: NSValue(caTransform3D: snapshotTransform), to: NSValue(caTransform3D: targetTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
|
||||
snapshotView.alpha = 1.0
|
||||
snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
|
||||
self.previewContainerView.addSubview(snapshotView)
|
||||
@ -2036,6 +2170,23 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.controller?.present(tooltipController, in: .current)
|
||||
}
|
||||
|
||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
|
||||
return
|
||||
}
|
||||
|
||||
let progress = 1.0 - value
|
||||
let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
let targetTopInset = ceil((layout.statusBarHeight ?? 0.0) - (layout.size.height - layout.size.height * maxScale) / 2.0)
|
||||
let deltaOffset = (targetTopInset - topInset)
|
||||
|
||||
let scale = 1.0 * progress + (1.0 - progress) * maxScale
|
||||
let offset = (1.0 - progress) * deltaOffset
|
||||
transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result == self.componentHost.view {
|
||||
@ -2045,24 +2196,44 @@ public final class MediaEditorScreen: ViewController {
|
||||
return result
|
||||
}
|
||||
|
||||
func requestUpdate(transition: Transition = .immediate) {
|
||||
func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: transition)
|
||||
self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private var drawingScreen: DrawingScreen?
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
|
||||
private var stickerScreen: StickerPickerScreen?
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
|
||||
guard let controller = self.controller, !self.isDismissed else {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
let previewSize: CGSize
|
||||
if isTablet {
|
||||
let previewHeight = layout.size.height - topInset - 75.0
|
||||
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
|
||||
} else {
|
||||
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
}
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
var inputHeight = layout.inputHeight ?? 0.0
|
||||
if self.stickerScreen != nil {
|
||||
inputHeight = 0.0
|
||||
}
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
navigationHeight: 0.0,
|
||||
@ -2072,7 +2243,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
bottom: bottomInset,
|
||||
right: layout.safeInsets.right
|
||||
),
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
inputHeight: inputHeight,
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
orientation: nil,
|
||||
@ -2084,6 +2255,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
return self?.controller
|
||||
}
|
||||
)
|
||||
|
||||
if hasAppeared && !self.hasAppeared {
|
||||
self.hasAppeared = hasAppeared
|
||||
}
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
transition: transition,
|
||||
@ -2093,6 +2268,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
isDisplayingTool: self.isDisplayingTool,
|
||||
isInteractingWithEntities: self.isInteractingWithEntities,
|
||||
isSavingAvailable: controller.isSavingAvailable,
|
||||
hasAppeared: self.hasAppeared,
|
||||
isDismissing: self.isDismissing,
|
||||
mediaEditor: self.mediaEditor,
|
||||
privacy: controller.state.privacy,
|
||||
@ -2113,14 +2289,24 @@ public final class MediaEditorScreen: ViewController {
|
||||
case .sticker:
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
|
||||
controller.completion = { [weak self] file in
|
||||
if let self, let file {
|
||||
let stickerEntity = DrawingStickerEntity(content: .file(file))
|
||||
self.interaction?.insertEntity(stickerEntity)
|
||||
|
||||
self.controller?.isSavingAvailable = true
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
if let self {
|
||||
if let file {
|
||||
let stickerEntity = DrawingStickerEntity(content: .file(file))
|
||||
self.interaction?.insertEntity(stickerEntity)
|
||||
|
||||
self.controller?.isSavingAvailable = true
|
||||
self.controller?.requestLayout(transition: .immediate)
|
||||
}
|
||||
self.stickerScreen = nil
|
||||
}
|
||||
}
|
||||
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
|
||||
if let self, let controller {
|
||||
let transitionFactor = controller.modalStyleOverlayTransitionFactor
|
||||
self.updateModalTransitionFactor(transitionFactor, transition: transition)
|
||||
}
|
||||
}
|
||||
self.stickerScreen = controller
|
||||
self.controller?.present(controller, in: .current)
|
||||
return
|
||||
case .text:
|
||||
@ -2189,12 +2375,12 @@ public final class MediaEditorScreen: ViewController {
|
||||
environment: {
|
||||
environment
|
||||
},
|
||||
forceUpdate: forceUpdate || animateOut,
|
||||
forceUpdate: forceUpdate,
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let componentView = self.componentHost.view {
|
||||
if componentView.superview == nil {
|
||||
self.view.insertSubview(componentView, at: 3)
|
||||
self.containerView.addSubview(componentView)
|
||||
componentView.clipsToBounds = true
|
||||
}
|
||||
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize))
|
||||
@ -2248,15 +2434,20 @@ public final class MediaEditorScreen: ViewController {
|
||||
transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0)
|
||||
|
||||
var bottomInputOffset: CGFloat = 0.0
|
||||
if let inputHeight = layout.inputHeight, inputHeight > 0.0 {
|
||||
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
||||
bottomInputOffset = inputHeight / 2.0
|
||||
} else {
|
||||
bottomInputOffset = inputHeight - bottomInset - 17.0
|
||||
if inputHeight > 0.0 {
|
||||
if self.stickerScreen == nil {
|
||||
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
||||
bottomInputOffset = inputHeight / 2.0
|
||||
} else {
|
||||
bottomInputOffset = inputHeight - bottomInset - 17.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize)
|
||||
transition.setPosition(view: self.containerView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
let previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize)
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
let entitiesViewScale = previewSize.width / storyDimensions.width
|
||||
self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale)
|
||||
@ -2278,15 +2469,35 @@ public final class MediaEditorScreen: ViewController {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
public enum PIPPosition {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomLeft
|
||||
case bottomRight
|
||||
|
||||
func getPosition(_ size: CGSize) -> CGPoint {
|
||||
switch self {
|
||||
case .topLeft:
|
||||
return CGPoint(x: 224.0, y: 477.0)
|
||||
case .topRight:
|
||||
return CGPoint(x: size.width - 224.0, y: 477.0)
|
||||
case .bottomLeft:
|
||||
return CGPoint(x: 224.0, y: size.height - 477.0)
|
||||
case .bottomRight:
|
||||
return CGPoint(x: size.width - 224.0, y: size.height - 477.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Subject {
|
||||
case image(UIImage, PixelDimensions, UIImage?)
|
||||
case video(String, UIImage?, PixelDimensions)
|
||||
case image(UIImage, PixelDimensions, UIImage?, PIPPosition)
|
||||
case video(String, UIImage?, String?, UIImage?, PixelDimensions, PIPPosition)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft, Int64?)
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
case let .image(_, dimensions, _), let .video(_, _, dimensions):
|
||||
case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _):
|
||||
return dimensions
|
||||
case let .asset(asset):
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
@ -2297,9 +2508,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
var editorSubject: MediaEditor.Subject {
|
||||
switch self {
|
||||
case let .image(image, dimensions, _):
|
||||
case let .image(image, dimensions, _, _):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, transitionImage, dimensions):
|
||||
case let .video(videoPath, transitionImage, _, _, dimensions, _):
|
||||
return .video(videoPath, transitionImage, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
@ -2310,9 +2521,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
var mediaContent: DrawingMediaEntity.Content {
|
||||
switch self {
|
||||
case let .image(image, dimensions, _):
|
||||
case let .image(image, dimensions, _, _):
|
||||
return .image(image, dimensions)
|
||||
case let .video(videoPath, _, dimensions):
|
||||
case let .video(videoPath, _, _, _, dimensions, _):
|
||||
return .video(videoPath, dimensions)
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
@ -2381,6 +2592,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.displayNode = Node(controller: self)
|
||||
|
||||
super.displayNodeDidLoad()
|
||||
|
||||
let dropInteraction = UIDropInteraction(delegate: self)
|
||||
self.displayNode.view.addInteraction(dropInteraction)
|
||||
}
|
||||
|
||||
func openPrivacySettings() {
|
||||
@ -2735,9 +2949,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case let .image(image, dimensions, _):
|
||||
case let .image(image, dimensions, _, _):
|
||||
saveImageDraft(image, dimensions)
|
||||
case let .video(path, _, dimensions):
|
||||
case let .video(path, _, _, _, dimensions, _):
|
||||
saveVideoDraft(path, dimensions)
|
||||
case let .asset(asset):
|
||||
if asset.mediaType == .video {
|
||||
@ -2780,6 +2994,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
mediaEditor.seek(0.0, andPlay: false)
|
||||
mediaEditor.invalidate()
|
||||
self.node.entitiesView.invalidate()
|
||||
|
||||
@ -2802,14 +3017,14 @@ public final class MediaEditorScreen: ViewController {
|
||||
let videoResult: Result.VideoResult
|
||||
let duration: Double
|
||||
switch subject {
|
||||
case let .image(image, _, _):
|
||||
case let .image(image, _, _, _):
|
||||
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
||||
if let data = image.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
||||
}
|
||||
videoResult = .imageFile(path: tempImagePath)
|
||||
duration = 5.0
|
||||
case let .video(path, _, _):
|
||||
case let .video(path, _, _, _, _, _):
|
||||
videoResult = .videoFile(path: path)
|
||||
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
||||
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
||||
@ -2840,14 +3055,20 @@ public final class MediaEditorScreen: ViewController {
|
||||
duration = 5.0
|
||||
}
|
||||
}
|
||||
self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in
|
||||
// if let self {
|
||||
self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
// }
|
||||
// })
|
||||
|
||||
if case let .draft(draft, id) = subject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
@ -2925,10 +3146,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
let exportSubject: Signal<MediaEditorVideoExport.Subject, NoError>
|
||||
switch subject {
|
||||
case let .video(path, _, _):
|
||||
case let .video(path, _, _, _, _, _):
|
||||
let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL)
|
||||
exportSubject = .single(.video(asset))
|
||||
case let .image(image, _, _):
|
||||
case let .image(image, _, _, _):
|
||||
exportSubject = .single(.image(image))
|
||||
case let .asset(asset):
|
||||
exportSubject = Signal { subscriber in
|
||||
@ -3061,21 +3282,60 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
|
||||
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String])
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
|
||||
let operation: UIDropOperation
|
||||
operation = .copy
|
||||
return UIDropProposal(operation: operation)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
|
||||
session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let images = imageItems as! [UIImage]
|
||||
if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 {
|
||||
self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
|
||||
}
|
||||
}
|
||||
|
||||
final class PrivacyButtonComponent: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let icon: UIImage
|
||||
let text: String
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
icon: UIImage,
|
||||
text: String
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.icon = icon
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool {
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
@ -3106,7 +3366,7 @@ final class PrivacyButtonComponent: CombinedComponent {
|
||||
|
||||
let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0)
|
||||
let background = background.update(
|
||||
component: BlurredBackgroundComponent(color: UIColor(white: 0.0, alpha: 0.5)),
|
||||
component: BlurredBackgroundComponent(color: context.component.backgroundColor),
|
||||
availableSize: backgroundSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
@ -323,16 +323,33 @@ private final class MediaToolsScreenComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
let mediaEditor = (environment.controller() as? MediaToolsScreen)?.mediaEditor
|
||||
|
||||
let sectionUpdated = component.sectionUpdated
|
||||
|
||||
let previewContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.safeInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom))
|
||||
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom))
|
||||
|
||||
let buttonSideInset: CGFloat = 10.0
|
||||
|
||||
let buttonSideInset: CGFloat
|
||||
let buttonBottomInset: CGFloat = 8.0
|
||||
|
||||
let previewSize: CGSize
|
||||
let topInset: CGFloat = environment.statusBarHeight + 12.0
|
||||
if isTablet {
|
||||
let previewHeight = availableSize.height - topInset - 75.0
|
||||
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
|
||||
buttonSideInset = 30.0
|
||||
} else {
|
||||
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
|
||||
buttonSideInset = 10.0
|
||||
}
|
||||
|
||||
let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom))
|
||||
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom))
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
@ -396,6 +413,16 @@ private final class MediaToolsScreenComponent: Component {
|
||||
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
|
||||
}
|
||||
|
||||
let buttonsAvailableWidth: CGFloat
|
||||
let buttonsLeftOffset: CGFloat
|
||||
if isTablet {
|
||||
buttonsAvailableWidth = previewSize.width + 260.0
|
||||
buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0)
|
||||
} else {
|
||||
buttonsAvailableWidth = availableSize.width
|
||||
buttonsLeftOffset = 0.0
|
||||
}
|
||||
|
||||
let adjustmentsButtonSize = self.adjustmentsButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
@ -412,7 +439,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let adjustmentsButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
size: adjustmentsButtonSize
|
||||
)
|
||||
if let adjustmentsButtonView = self.adjustmentsButton.view {
|
||||
@ -438,7 +465,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let tintButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
size: tintButtonSize
|
||||
)
|
||||
if let tintButtonView = self.tintButton.view {
|
||||
@ -464,7 +491,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let blurButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
size: blurButtonSize
|
||||
)
|
||||
if let blurButtonView = self.blurButton.view {
|
||||
@ -490,7 +517,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
let curvesButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset),
|
||||
size: curvesButtonSize
|
||||
)
|
||||
if let curvesButtonView = self.curvesButton.view {
|
||||
@ -640,10 +667,31 @@ private final class MediaToolsScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: previewContainerFrame.size
|
||||
)
|
||||
|
||||
let adjustmentsToolScreen: ComponentView<Empty>
|
||||
if let current = self.toolScreen, !sectionChanged {
|
||||
adjustmentsToolScreen = current
|
||||
} else {
|
||||
adjustmentsToolScreen = ComponentView<Empty>()
|
||||
self.toolScreen = adjustmentsToolScreen
|
||||
}
|
||||
toolScreen = adjustmentsToolScreen
|
||||
screenSize = adjustmentsToolScreen.update(
|
||||
transition: optionsTransition,
|
||||
component: AnyComponent(
|
||||
AdjustmentsScreenComponent(
|
||||
toggleUneditedPreview: { preview in
|
||||
if let controller = environment.controller() as? MediaToolsScreen {
|
||||
controller.mediaEditor.setPreviewUnedited(preview)
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
screenSize = previewContainerFrame.size
|
||||
self.toolScreen = nil
|
||||
case .tint:
|
||||
self.curvesState = nil
|
||||
optionsSize = self.toolOptions.update(
|
||||
@ -676,10 +724,31 @@ private final class MediaToolsScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: previewContainerFrame.size
|
||||
)
|
||||
|
||||
let tintToolScreen: ComponentView<Empty>
|
||||
if let current = self.toolScreen, !sectionChanged {
|
||||
tintToolScreen = current
|
||||
} else {
|
||||
tintToolScreen = ComponentView<Empty>()
|
||||
self.toolScreen = tintToolScreen
|
||||
}
|
||||
toolScreen = tintToolScreen
|
||||
screenSize = tintToolScreen.update(
|
||||
transition: optionsTransition,
|
||||
component: AnyComponent(
|
||||
AdjustmentsScreenComponent(
|
||||
toggleUneditedPreview: { preview in
|
||||
if let controller = environment.controller() as? MediaToolsScreen {
|
||||
controller.mediaEditor.setPreviewUnedited(preview)
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
screenSize = previewContainerFrame.size
|
||||
self.toolScreen = nil
|
||||
case .blur:
|
||||
self.curvesState = nil
|
||||
optionsSize = self.toolOptions.update(
|
||||
@ -706,7 +775,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: previewContainerFrame.size
|
||||
)
|
||||
|
||||
let blurToolScreen: ComponentView<Empty>
|
||||
@ -764,7 +833,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
internalState: internalState
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: previewContainerFrame.size
|
||||
)
|
||||
|
||||
let curvesToolScreen: ComponentView<Empty>
|
||||
@ -917,9 +986,22 @@ public final class MediaToolsScreen: ViewController {
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let isTablet: Bool
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
isTablet = true
|
||||
} else {
|
||||
isTablet = false
|
||||
}
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let previewSize: CGSize
|
||||
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0
|
||||
if isTablet {
|
||||
let previewHeight = layout.size.height - topInset - 75.0
|
||||
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
|
||||
} else {
|
||||
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
}
|
||||
let bottomInset = layout.size.height - previewSize.height - topInset
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
@ -944,13 +1026,6 @@ public final class MediaToolsScreen: ViewController {
|
||||
}
|
||||
)
|
||||
|
||||
// var transition = transition
|
||||
// if isFirstTime {
|
||||
// transition = transition.withUserData(CameraScreenTransition.animateIn)
|
||||
// } else if animateOut {
|
||||
// transition = transition.withUserData(CameraScreenTransition.animateOut)
|
||||
// }
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
|
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "off shadow.pdf",
|
||||
"filename" : "off.pdf",
|
||||
"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" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,8 @@
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "flash.pdf",
|
||||
"filename" : "magnifying.pdf",
|
||||
"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)
|
||||
}
|
||||
|
||||
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed)
|
||||
public func makeMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed)
|
||||
}
|
||||
|
||||
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
|
||||
|
@ -27,6 +27,7 @@ import LocalMediaResources
|
||||
import ShareWithPeersScreen
|
||||
import ImageCompression
|
||||
import TextFormat
|
||||
import UndoUI
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
@ -254,6 +255,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
controller.view.endEditing(true)
|
||||
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var returnToCameraImpl: (() -> Void)?
|
||||
@ -287,13 +289,25 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
completion: { result, resultTransition, dismissed in
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreen.Subject? in
|
||||
func editorPIPPosition(_ position: CameraScreen.PIPPosition) -> MediaEditorScreen.PIPPosition {
|
||||
switch position {
|
||||
case .topLeft:
|
||||
return .topLeft
|
||||
case .topRight:
|
||||
return .topRight
|
||||
case .bottomLeft:
|
||||
return .bottomLeft
|
||||
case .bottomRight:
|
||||
return .bottomRight
|
||||
}
|
||||
}
|
||||
switch value {
|
||||
case .pendingImage:
|
||||
return nil
|
||||
case let .image(image, additionalImage):
|
||||
return .image(image, PixelDimensions(image.size), additionalImage)
|
||||
case let .video(path, transitionImage, dimensions):
|
||||
return .video(path, transitionImage, dimensions)
|
||||
case let .image(image, additionalImage, pipPosition):
|
||||
return .image(image, PixelDimensions(image.size), additionalImage, editorPIPPosition(pipPosition))
|
||||
case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, pipPosition):
|
||||
return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, editorPIPPosition(pipPosition))
|
||||
case let .asset(asset):
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
@ -362,6 +376,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
|
||||
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in
|
||||
switch action {
|
||||
case .undo:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
})
|
||||
chatListController.present(undoOverlayController, in: .current)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -420,7 +445,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
commit({})
|
||||
}
|
||||
}
|
||||
case let .video(content, _, values, duration, dimensions, caption):
|
||||
case let .video(content, image, values, duration, dimensions, caption):
|
||||
let adjustments: VideoMediaResourceAdjustments
|
||||
if let valuesData = try? JSONEncoder().encode(values) {
|
||||
let data = MemoryBuffer(data: valuesData)
|
||||
@ -451,6 +476,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
|
||||
if let image {
|
||||
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in
|
||||
switch action {
|
||||
case .undo:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
})
|
||||
chatListController.present(undoOverlayController, in: .current)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user