mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
Merge commit 'f11c45070e5e533f6f4077cc9f7708afd443538b' into HEAD
This commit is contained in:
@@ -774,8 +774,25 @@ public struct StoryCameraTransitionOut {
|
||||
}
|
||||
}
|
||||
|
||||
public struct StoryCameraTransitionInCoordinator {
|
||||
public let animateIn: () -> Void
|
||||
public let updateTransitionProgress: (CGFloat) -> Void
|
||||
public let completeWithTransitionProgressAndVelocity: (CGFloat, CGFloat) -> Void
|
||||
|
||||
public init(
|
||||
animateIn: @escaping () -> Void,
|
||||
updateTransitionProgress: @escaping (CGFloat) -> Void,
|
||||
completeWithTransitionProgressAndVelocity: @escaping (CGFloat, CGFloat) -> Void
|
||||
) {
|
||||
self.animateIn = animateIn
|
||||
self.updateTransitionProgress = updateTransitionProgress
|
||||
self.completeWithTransitionProgressAndVelocity = completeWithTransitionProgressAndVelocity
|
||||
}
|
||||
}
|
||||
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?)
|
||||
@discardableResult
|
||||
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
@@ -874,7 +891,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) -> Void) -> ViewController
|
||||
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -40,15 +40,16 @@ private final class CameraContext {
|
||||
}
|
||||
}
|
||||
|
||||
private let previewSnapshotContext = CIContext()
|
||||
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
|
||||
private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) {
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let ciContext = CIContext()
|
||||
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33))
|
||||
if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let size = ciImage.extent.size
|
||||
ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size))
|
||||
if let cgImage = self.previewSnapshotContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
CameraSimplePreviewView.saveLastState(uiImage)
|
||||
CameraSimplePreviewView.saveLastStateImage(uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,7 @@ private final class CameraContext {
|
||||
self.input.configure(for: self.session, device: self.device, audio: configuration.audio)
|
||||
self.output.configure(for: self.session, configuration: configuration)
|
||||
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60)
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate)
|
||||
self.output.configureVideoStabilization()
|
||||
}
|
||||
|
||||
@@ -114,6 +115,15 @@ private final class CameraContext {
|
||||
}
|
||||
}
|
||||
|
||||
private var preferredMaxFrameRate: Double {
|
||||
switch DeviceModel.current {
|
||||
case .iPhone14ProMax, .iPhone13ProMax:
|
||||
return 60.0
|
||||
default:
|
||||
return 30.0
|
||||
}
|
||||
}
|
||||
|
||||
func startCapture() {
|
||||
guard !self.session.isRunning else {
|
||||
return
|
||||
@@ -132,8 +142,17 @@ private final class CameraContext {
|
||||
self.session.stopRunning()
|
||||
}
|
||||
|
||||
func focus(at point: CGPoint) {
|
||||
self.device.setFocusPoint(point, focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: true)
|
||||
func focus(at point: CGPoint, autoFocus: Bool) {
|
||||
let focusMode: AVCaptureDevice.FocusMode
|
||||
let exposureMode: AVCaptureDevice.ExposureMode
|
||||
if autoFocus {
|
||||
focusMode = .continuousAutoFocus
|
||||
exposureMode = .continuousAutoExposure
|
||||
} else {
|
||||
focusMode = .autoFocus
|
||||
exposureMode = .autoExpose
|
||||
}
|
||||
self.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true)
|
||||
}
|
||||
|
||||
func setFps(_ fps: Float64) {
|
||||
@@ -159,7 +178,7 @@ private final class CameraContext {
|
||||
self.changingPosition = true
|
||||
self.device.configure(for: self.session, position: targetPosition)
|
||||
self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio)
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60)
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate)
|
||||
self.output.configureVideoStabilization()
|
||||
self.queue.after(0.5) {
|
||||
self.changingPosition = false
|
||||
@@ -172,7 +191,7 @@ private final class CameraContext {
|
||||
self.input.invalidate(for: self.session)
|
||||
self.device.configure(for: self.session, position: position)
|
||||
self.input.configure(for: self.session, device: self.device, audio: self.initialConfiguration.audio)
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: 60)
|
||||
self.device.configureDeviceFormat(maxDimensions: CMVideoDimensions(width: 1920, height: 1080), maxFramerate: self.preferredMaxFrameRate)
|
||||
self.output.configureVideoStabilization()
|
||||
}
|
||||
}
|
||||
@@ -266,6 +285,9 @@ public final class Camera {
|
||||
self.metrics = Camera.Metrics(model: DeviceModel.current)
|
||||
|
||||
let session = AVCaptureSession()
|
||||
session.usesApplicationAudioSession = true
|
||||
session.automaticallyConfiguresApplicationAudioSession = false
|
||||
session.automaticallyConfiguresCaptureDeviceForWideColor = false
|
||||
if let previewView {
|
||||
previewView.session = session
|
||||
}
|
||||
@@ -363,10 +385,10 @@ public final class Camera {
|
||||
}
|
||||
}
|
||||
|
||||
public func focus(at point: CGPoint) {
|
||||
public func focus(at point: CGPoint, autoFocus: Bool = true) {
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.focus(at: point)
|
||||
context.focus(at: point, autoFocus: autoFocus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
private let defaultFPS: Double = 30.0
|
||||
|
||||
final class CameraDevice {
|
||||
var position: Camera.Position = .back
|
||||
|
||||
deinit {
|
||||
if let videoDevice = self.videoDevice {
|
||||
self.unsubscribeFromChanges(videoDevice)
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var videoDevice: AVCaptureDevice? = nil {
|
||||
didSet {
|
||||
if let previousVideoDevice = oldValue {
|
||||
self.unsubscribeFromChanges(previousVideoDevice)
|
||||
}
|
||||
self.videoDevicePromise.set(.single(self.videoDevice))
|
||||
if let videoDevice = self.videoDevice {
|
||||
self.subscribeForChanges(videoDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
private var videoDevicePromise = Promise<AVCaptureDevice?>()
|
||||
@@ -68,6 +81,14 @@ final class CameraDevice {
|
||||
|
||||
if let bestFormat = candidates.last {
|
||||
device.activeFormat = bestFormat
|
||||
|
||||
Logger.shared.log("Camera", "Available formats:")
|
||||
for format in device.formats {
|
||||
Logger.shared.log("Camera", format.description)
|
||||
}
|
||||
|
||||
Logger.shared.log("Camera", "Selected format:")
|
||||
Logger.shared.log("Camera", bestFormat.description)
|
||||
}
|
||||
|
||||
if let targetFPS = device.actualFPS(maxFramerate) {
|
||||
@@ -84,12 +105,12 @@ final class CameraDevice {
|
||||
}
|
||||
}
|
||||
|
||||
private func subscribeForChanges() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice)
|
||||
private func subscribeForChanges(_ device: AVCaptureDevice) {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device)
|
||||
}
|
||||
|
||||
private func unsubscribeFromChanges() {
|
||||
NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice)
|
||||
private func unsubscribeFromChanges(_ device: AVCaptureDevice) {
|
||||
NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: device)
|
||||
}
|
||||
|
||||
@objc private func subjectAreaChanged() {
|
||||
@@ -162,6 +183,12 @@ final class CameraDevice {
|
||||
device.focusPointOfInterest = point
|
||||
device.focusMode = focusMode
|
||||
}
|
||||
|
||||
device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
|
||||
|
||||
if abs(device.exposureTargetBias) > 0.0 {
|
||||
device.setExposureTargetBias(0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ public extension Camera {
|
||||
self = .iPhone14ProMax
|
||||
case .unknown:
|
||||
self = .unknown
|
||||
default:
|
||||
self = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +72,16 @@ enum DeviceModel: CaseIterable {
|
||||
case iPodTouch6
|
||||
case iPodTouch7
|
||||
|
||||
case iPhone12
|
||||
case iPhone12Mini
|
||||
case iPhone12Pro
|
||||
case iPhone12ProMax
|
||||
|
||||
case iPhone13
|
||||
case iPhone13Mini
|
||||
case iPhone13Pro
|
||||
case iPhone13ProMax
|
||||
|
||||
case iPhone14
|
||||
case iPhone14Plus
|
||||
case iPhone14Pro
|
||||
@@ -93,6 +105,22 @@ enum DeviceModel: CaseIterable {
|
||||
return "iPod7,1"
|
||||
case .iPodTouch7:
|
||||
return "iPod9,1"
|
||||
case .iPhone12:
|
||||
return "iPhone13,2"
|
||||
case .iPhone12Mini:
|
||||
return "iPhone13,1"
|
||||
case .iPhone12Pro:
|
||||
return "iPhone13,3"
|
||||
case .iPhone12ProMax:
|
||||
return "iPhone13,4"
|
||||
case .iPhone13:
|
||||
return "iPhone14,5"
|
||||
case .iPhone13Mini:
|
||||
return "iPhone14,4"
|
||||
case .iPhone13Pro:
|
||||
return "iPhone14,2"
|
||||
case .iPhone13ProMax:
|
||||
return "iPhone14,3"
|
||||
case .iPhone14:
|
||||
return "iPhone14,7"
|
||||
case .iPhone14Plus:
|
||||
@@ -122,6 +150,22 @@ enum DeviceModel: CaseIterable {
|
||||
return "iPod touch 6G"
|
||||
case .iPodTouch7:
|
||||
return "iPod touch 7G"
|
||||
case .iPhone12:
|
||||
return "iPhone 12"
|
||||
case .iPhone12Mini:
|
||||
return "iPhone 12 mini"
|
||||
case .iPhone12Pro:
|
||||
return "iPhone 12 Pro"
|
||||
case .iPhone12ProMax:
|
||||
return "iPhone 12 Pro Max"
|
||||
case .iPhone13:
|
||||
return "iPhone 13"
|
||||
case .iPhone13Mini:
|
||||
return "iPhone 13 mini"
|
||||
case .iPhone13Pro:
|
||||
return "iPhone 13 Pro"
|
||||
case .iPhone13ProMax:
|
||||
return "iPhone 13 Pro Max"
|
||||
case .iPhone14:
|
||||
return "iPhone 14"
|
||||
case .iPhone14Plus:
|
||||
|
||||
@@ -65,7 +65,6 @@ final class CameraOutput: NSObject {
|
||||
super.init()
|
||||
|
||||
self.videoOutput.alwaysDiscardsLateVideoFrames = false
|
||||
//self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA] as [String : Any]
|
||||
self.videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange] as [String : Any]
|
||||
|
||||
self.faceLandmarksOutput.outputFaceObservations = { [weak self] observations in
|
||||
@@ -110,11 +109,12 @@ final class CameraOutput: NSObject {
|
||||
|
||||
func configureVideoStabilization() {
|
||||
if let videoDataOutputConnection = self.videoOutput.connection(with: .video), videoDataOutputConnection.isVideoStabilizationSupported {
|
||||
if #available(iOS 13.0, *) {
|
||||
videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended
|
||||
} else {
|
||||
videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic
|
||||
}
|
||||
videoDataOutputConnection.preferredVideoStabilizationMode = .standard
|
||||
// if #available(iOS 13.0, *) {
|
||||
// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematicExtended
|
||||
// } else {
|
||||
// videoDataOutputConnection.preferredVideoStabilizationMode = .cinematic
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ final class CameraOutput: NSObject {
|
||||
let outputFileName = NSUUID().uuidString
|
||||
let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4"
|
||||
let outputFileURL = URL(fileURLWithPath: outputFilePath)
|
||||
let videoRecorder = VideoRecorder(preset: MediaPreset(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in
|
||||
let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in
|
||||
if case .success = result {
|
||||
self?.recordingCompletionPipe.putNext(outputFilePath)
|
||||
} else {
|
||||
@@ -187,7 +187,8 @@ final class CameraOutput: NSObject {
|
||||
}
|
||||
})
|
||||
|
||||
videoRecorder.start()
|
||||
|
||||
videoRecorder?.start()
|
||||
self.videoRecorder = videoRecorder
|
||||
|
||||
return Signal { subscriber in
|
||||
@@ -245,13 +246,8 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA
|
||||
// self.processSampleBuffer?(finalVideoPixelBuffer, connection)
|
||||
// }
|
||||
|
||||
if let videoRecorder = self.videoRecorder, videoRecorder.isRecording || videoRecorder.isStopping {
|
||||
let mediaType = sampleBuffer.type
|
||||
if mediaType == kCMMediaType_Video {
|
||||
videoRecorder.appendVideo(sampleBuffer: sampleBuffer)
|
||||
} else if mediaType == kCMMediaType_Audio {
|
||||
videoRecorder.appendAudio(sampleBuffer: sampleBuffer)
|
||||
}
|
||||
if let videoRecorder = self.videoRecorder, videoRecorder.isRecording {
|
||||
videoRecorder.appendSampleBuffer(sampleBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ public class CameraSimplePreviewView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
static func saveLastState(_ image: UIImage) {
|
||||
static func saveLastStateImage(_ image: UIImage) {
|
||||
let imagePath = NSTemporaryDirectory() + "cameraImage.jpg"
|
||||
if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) {
|
||||
if let data = image.jpegData(compressionQuality: 0.6) {
|
||||
try? data.write(to: URL(fileURLWithPath: imagePath))
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,10 @@ public class CameraSimplePreviewView: UIView {
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
}
|
||||
|
||||
public func cameraPoint(for location: CGPoint) -> CGPoint {
|
||||
return self.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
|
||||
}
|
||||
}
|
||||
|
||||
public class CameraPreviewView: MTKView {
|
||||
|
||||
@@ -1,129 +1,439 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
struct MediaPreset {
|
||||
var videoSettings: [String: Any]
|
||||
var audioSettings: [String: Any]
|
||||
|
||||
init(videoSettings: [String: Any], audioSettings: [String: Any]) {
|
||||
self.videoSettings = videoSettings
|
||||
self.audioSettings = audioSettings
|
||||
}
|
||||
|
||||
var hasAudio: Bool {
|
||||
return !self.audioSettings.isEmpty
|
||||
private extension CMSampleBuffer {
|
||||
var endTime: CMTime {
|
||||
let presentationTime = CMSampleBufferGetPresentationTimeStamp(self)
|
||||
let duration = CMSampleBufferGetDuration(self)
|
||||
return presentationTime + duration
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoRecorder {
|
||||
private final class VideoRecorderImpl {
|
||||
public enum RecorderError: LocalizedError {
|
||||
case generic
|
||||
case avError(Error)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .generic:
|
||||
return "Error"
|
||||
case let .avError(error):
|
||||
return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let queue = DispatchQueue(label: "VideoRecorder")
|
||||
|
||||
private var assetWriter: AVAssetWriter
|
||||
private var videoInput: AVAssetWriterInput?
|
||||
private var audioInput: AVAssetWriterInput?
|
||||
|
||||
private var pendingAudioSampleBuffers: [CMSampleBuffer] = []
|
||||
|
||||
private var _duration: CMTime = .zero
|
||||
public var duration: CMTime {
|
||||
self.queue.sync { _duration }
|
||||
}
|
||||
|
||||
private var lastVideoSampleTime: CMTime = .invalid
|
||||
private var recordingStartSampleTime: CMTime = .invalid
|
||||
private var recordingStopSampleTime: CMTime = .invalid
|
||||
|
||||
private let configuration: VideoRecorder.Configuration
|
||||
private let videoTransform: CGAffineTransform
|
||||
private let url: URL
|
||||
fileprivate var completion: (Bool) -> Void = { _ in }
|
||||
|
||||
private let error = Atomic<Error?>(value: nil)
|
||||
|
||||
private var stopped = false
|
||||
private var hasAllVideoBuffers = false
|
||||
private var hasAllAudioBuffers = false
|
||||
|
||||
public init?(configuration: VideoRecorder.Configuration, videoTransform: CGAffineTransform, fileUrl: URL) {
|
||||
self.configuration = configuration
|
||||
self.videoTransform = videoTransform
|
||||
self.url = fileUrl
|
||||
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
|
||||
return nil
|
||||
}
|
||||
self.assetWriter = assetWriter
|
||||
self.assetWriter.shouldOptimizeForNetworkUse = false
|
||||
}
|
||||
|
||||
private func hasError() -> Error? {
|
||||
return self.error.with { $0 }
|
||||
}
|
||||
|
||||
public func start() {
|
||||
self.queue.async {
|
||||
self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
}
|
||||
}
|
||||
|
||||
public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||
if let _ = self.hasError() {
|
||||
return
|
||||
}
|
||||
|
||||
guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Video else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
self.queue.async {
|
||||
guard !self.stopped && self.error.with({ $0 }) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
var failed = false
|
||||
if self.videoInput == nil {
|
||||
let videoSettings = self.configuration.videoSettings
|
||||
if self.assetWriter.canApply(outputSettings: videoSettings, forMediaType: .video) {
|
||||
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings, sourceFormatHint: formatDescription)
|
||||
videoInput.expectsMediaDataInRealTime = true
|
||||
videoInput.transform = self.videoTransform
|
||||
if self.assetWriter.canAdd(videoInput) {
|
||||
self.assetWriter.add(videoInput)
|
||||
self.videoInput = videoInput
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
print("error")
|
||||
return
|
||||
}
|
||||
|
||||
if self.assetWriter.status == .unknown {
|
||||
if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime {
|
||||
return
|
||||
}
|
||||
if !self.assetWriter.startWriting() {
|
||||
if let error = self.assetWriter.error {
|
||||
self.transitionToFailedStatus(error: .avError(error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.assetWriter.startSession(atSourceTime: presentationTime)
|
||||
self.recordingStartSampleTime = presentationTime
|
||||
self.lastVideoSampleTime = presentationTime
|
||||
}
|
||||
|
||||
if self.assetWriter.status == .writing {
|
||||
if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime {
|
||||
self.hasAllVideoBuffers = true
|
||||
self.maybeFinish()
|
||||
return
|
||||
}
|
||||
|
||||
if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData {
|
||||
if videoInput.append(sampleBuffer) {
|
||||
self.lastVideoSampleTime = presentationTime
|
||||
let startTime = self.recordingStartSampleTime
|
||||
let duration = presentationTime - startTime
|
||||
self._duration = duration
|
||||
} else {
|
||||
print("error")
|
||||
}
|
||||
if !self.tryAppendingPendingAudioBuffers() {
|
||||
self.transitionToFailedStatus(error: .generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||
if let _ = self.hasError() {
|
||||
return
|
||||
}
|
||||
|
||||
guard let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer), CMFormatDescriptionGetMediaType(formatDescription) == kCMMediaType_Audio else {
|
||||
return
|
||||
}
|
||||
|
||||
self.queue.async {
|
||||
guard !self.stopped && self.error.with({ $0 }) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
var failed = false
|
||||
if self.audioInput == nil {
|
||||
var audioSettings = self.configuration.audioSettings
|
||||
if let currentAudioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) {
|
||||
audioSettings[AVSampleRateKey] = currentAudioStreamBasicDescription.pointee.mSampleRate
|
||||
audioSettings[AVNumberOfChannelsKey] = currentAudioStreamBasicDescription.pointee.mChannelsPerFrame
|
||||
}
|
||||
|
||||
var audioChannelLayoutSize: Int = 0
|
||||
let currentChannelLayout = CMAudioFormatDescriptionGetChannelLayout(formatDescription, sizeOut: &audioChannelLayoutSize)
|
||||
let currentChannelLayoutData: Data
|
||||
if let currentChannelLayout = currentChannelLayout, audioChannelLayoutSize > 0 {
|
||||
currentChannelLayoutData = Data(bytes: currentChannelLayout, count: audioChannelLayoutSize)
|
||||
} else {
|
||||
currentChannelLayoutData = Data()
|
||||
}
|
||||
audioSettings[AVChannelLayoutKey] = currentChannelLayoutData
|
||||
|
||||
if self.assetWriter.canApply(outputSettings: audioSettings, forMediaType: .audio) {
|
||||
let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings, sourceFormatHint: formatDescription)
|
||||
audioInput.expectsMediaDataInRealTime = true
|
||||
if self.assetWriter.canAdd(audioInput) {
|
||||
self.assetWriter.add(audioInput)
|
||||
self.audioInput = audioInput
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
|
||||
if failed {
|
||||
print("error")
|
||||
return
|
||||
}
|
||||
|
||||
if self.assetWriter.status == .writing {
|
||||
if sampleBuffer.presentationTimestamp < self.recordingStartSampleTime {
|
||||
return
|
||||
}
|
||||
if self.recordingStopSampleTime != .invalid && sampleBuffer.presentationTimestamp > self.recordingStopSampleTime {
|
||||
self.hasAllAudioBuffers = true
|
||||
self.maybeFinish()
|
||||
return
|
||||
}
|
||||
var result = false
|
||||
if self.tryAppendingPendingAudioBuffers() {
|
||||
if self.tryAppendingAudioSampleBuffer(sampleBuffer) {
|
||||
result = true
|
||||
}
|
||||
}
|
||||
if !result {
|
||||
self.transitionToFailedStatus(error: .generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func cancelRecording(completion: @escaping () -> Void) {
|
||||
self.queue.async {
|
||||
if self.stopped {
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
self.stopped = true
|
||||
self.pendingAudioSampleBuffers = []
|
||||
if self.assetWriter.status == .writing {
|
||||
self.assetWriter.cancelWriting()
|
||||
}
|
||||
let fileManager = FileManager()
|
||||
try? fileManager.removeItem(at: self.url)
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isRecording: Bool {
|
||||
self.queue.sync { !(self.hasAllVideoBuffers && self.hasAllAudioBuffers) }
|
||||
}
|
||||
|
||||
public func stopRecording() {
|
||||
self.queue.async {
|
||||
self.recordingStopSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
}
|
||||
}
|
||||
|
||||
public func maybeFinish() {
|
||||
self.queue.async {
|
||||
guard self.hasAllVideoBuffers && self.hasAllVideoBuffers else {
|
||||
return
|
||||
}
|
||||
self.stopped = true
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
public func finish() {
|
||||
self.queue.async {
|
||||
let completion = self.completion
|
||||
if self.recordingStopSampleTime == .invalid {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = self.error.with({ $0 }) {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !self.tryAppendingPendingAudioBuffers() {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if self.assetWriter.status == .writing {
|
||||
self.assetWriter.finishWriting {
|
||||
if let _ = self.assetWriter.error {
|
||||
DispatchQueue.main.async {
|
||||
completion(false)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let _ = self.assetWriter.error {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tryAppendingPendingAudioBuffers() -> Bool {
|
||||
dispatchPrecondition(condition: .onQueue(self.queue))
|
||||
guard self.pendingAudioSampleBuffers.count > 0 else {
|
||||
return true
|
||||
}
|
||||
|
||||
var result = true
|
||||
let (sampleBuffersToAppend, pendingSampleBuffers) = self.pendingAudioSampleBuffers.stableGroup(using: { $0.endTime <= self.lastVideoSampleTime })
|
||||
for sampleBuffer in sampleBuffersToAppend {
|
||||
if !self.internalAppendAudioSampleBuffer(sampleBuffer) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
}
|
||||
self.pendingAudioSampleBuffers = pendingSampleBuffers
|
||||
return result
|
||||
}
|
||||
|
||||
private func tryAppendingAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
||||
dispatchPrecondition(condition: .onQueue(self.queue))
|
||||
|
||||
var result = true
|
||||
if sampleBuffer.endTime > self.lastVideoSampleTime {
|
||||
self.pendingAudioSampleBuffers.append(sampleBuffer)
|
||||
} else {
|
||||
result = self.internalAppendAudioSampleBuffer(sampleBuffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func internalAppendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
||||
if let audioInput = self.audioInput, audioInput.isReadyForMoreMediaData {
|
||||
if !audioInput.append(sampleBuffer) {
|
||||
if let _ = self.assetWriter.error {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func transitionToFailedStatus(error: RecorderError) {
|
||||
let _ = self.error.modify({ _ in return error })
|
||||
}
|
||||
}
|
||||
|
||||
private extension Sequence {
|
||||
func stableGroup(using predicate: (Element) throws -> Bool) rethrows -> ([Element], [Element]) {
|
||||
var trueGroup: [Element] = []
|
||||
var falseGroup: [Element] = []
|
||||
for element in self {
|
||||
if try predicate(element) {
|
||||
trueGroup.append(element)
|
||||
} else {
|
||||
falseGroup.append(element)
|
||||
}
|
||||
}
|
||||
return (trueGroup, falseGroup)
|
||||
}
|
||||
}
|
||||
|
||||
public final class VideoRecorder {
|
||||
var duration: Double? {
|
||||
return self.impl.duration.seconds
|
||||
}
|
||||
|
||||
enum Result {
|
||||
enum Error {
|
||||
case generic
|
||||
}
|
||||
|
||||
case success
|
||||
case initError(Error)
|
||||
case writeError(Error)
|
||||
case finishError(Error)
|
||||
}
|
||||
|
||||
struct Configuration {
|
||||
var videoSettings: [String: Any]
|
||||
var audioSettings: [String: Any]
|
||||
|
||||
init(videoSettings: [String: Any], audioSettings: [String: Any]) {
|
||||
self.videoSettings = videoSettings
|
||||
self.audioSettings = audioSettings
|
||||
}
|
||||
|
||||
var hasAudio: Bool {
|
||||
return !self.audioSettings.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
private let impl: VideoRecorderImpl
|
||||
fileprivate let configuration: Configuration
|
||||
fileprivate let videoTransform: CGAffineTransform
|
||||
fileprivate let fileUrl: URL
|
||||
private let completion: (Result) -> Void
|
||||
|
||||
private let queue = Queue()
|
||||
private var assetWriter: AVAssetWriter?
|
||||
public var isRecording: Bool {
|
||||
return self.impl.isRecording
|
||||
}
|
||||
|
||||
private var videoInput: AVAssetWriterInput?
|
||||
private var audioInput: AVAssetWriterInput?
|
||||
|
||||
private let preset: MediaPreset
|
||||
private let videoTransform: CGAffineTransform
|
||||
private let fileUrl: URL
|
||||
|
||||
private (set) var isRecording = false
|
||||
private (set) var isStopping = false
|
||||
private var finishedWriting = false
|
||||
|
||||
private var captureStartTimestamp: Double?
|
||||
private var firstVideoTimestamp: CMTime?
|
||||
private var lastVideoTimestamp: CMTime?
|
||||
private var lastAudioTimestamp: CMTime?
|
||||
|
||||
private var pendingAudioBuffers: [CMSampleBuffer] = []
|
||||
|
||||
init(preset: MediaPreset, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) {
|
||||
self.preset = preset
|
||||
init?(configuration: Configuration, videoTransform: CGAffineTransform, fileUrl: URL, completion: @escaping (Result) -> Void) {
|
||||
self.configuration = configuration
|
||||
self.videoTransform = videoTransform
|
||||
self.fileUrl = fileUrl
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func start() {
|
||||
self.queue.async {
|
||||
guard self.assetWriter == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.captureStartTimestamp = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
guard let assetWriter = try? AVAssetWriter(url: self.fileUrl, fileType: .mp4) else {
|
||||
return
|
||||
}
|
||||
|
||||
let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: self.preset.videoSettings)
|
||||
videoInput.expectsMediaDataInRealTime = true
|
||||
videoInput.transform = self.videoTransform
|
||||
if assetWriter.canAdd(videoInput) {
|
||||
assetWriter.add(videoInput)
|
||||
}
|
||||
|
||||
let audioInput: AVAssetWriterInput?
|
||||
if self.preset.hasAudio {
|
||||
audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: self.preset.audioSettings)
|
||||
audioInput!.expectsMediaDataInRealTime = true
|
||||
if assetWriter.canAdd(audioInput!) {
|
||||
assetWriter.add(audioInput!)
|
||||
}
|
||||
} else {
|
||||
audioInput = nil
|
||||
}
|
||||
|
||||
self.assetWriter = assetWriter
|
||||
self.videoInput = videoInput
|
||||
self.audioInput = audioInput
|
||||
|
||||
self.isRecording = true
|
||||
|
||||
assetWriter.startWriting()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.queue.async {
|
||||
guard let captureStartTimestamp = self.captureStartTimestamp, abs(CFAbsoluteTimeGetCurrent() - captureStartTimestamp) > 0.5 else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isStopping = true
|
||||
|
||||
if self.audioInput == nil {
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finish() {
|
||||
guard let assetWriter = self.assetWriter else {
|
||||
return
|
||||
}
|
||||
|
||||
self.queue.async {
|
||||
self.isRecording = false
|
||||
self.isStopping = false
|
||||
|
||||
assetWriter.finishWriting {
|
||||
self.finishedWriting = true
|
||||
|
||||
if case .completed = assetWriter.status {
|
||||
guard let impl = VideoRecorderImpl(configuration: configuration, videoTransform: videoTransform, fileUrl: fileUrl) else {
|
||||
completion(.initError(.generic))
|
||||
return nil
|
||||
}
|
||||
self.impl = impl
|
||||
impl.completion = { [weak self] success in
|
||||
if let self {
|
||||
if success {
|
||||
self.completion(.success)
|
||||
} else {
|
||||
self.completion(.finishError(.generic))
|
||||
@@ -132,78 +442,25 @@ final class VideoRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
private var skippedCount = 0
|
||||
func appendVideo(sampleBuffer: CMSampleBuffer) {
|
||||
if self.skippedCount < 2 {
|
||||
self.skippedCount += 1
|
||||
func start() {
|
||||
self.impl.start()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.impl.stopRecording()
|
||||
}
|
||||
|
||||
func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
|
||||
guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else {
|
||||
return
|
||||
}
|
||||
self.queue.async {
|
||||
guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else {
|
||||
return
|
||||
}
|
||||
let timestamp = sampleBuffer.presentationTimestamp
|
||||
|
||||
switch assetWriter.status {
|
||||
case .unknown:
|
||||
break
|
||||
case .writing:
|
||||
if self.firstVideoTimestamp == nil {
|
||||
self.firstVideoTimestamp = timestamp
|
||||
assetWriter.startSession(atSourceTime: timestamp)
|
||||
}
|
||||
while !videoInput.isReadyForMoreMediaData {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
|
||||
}
|
||||
|
||||
if videoInput.append(sampleBuffer) {
|
||||
self.lastVideoTimestamp = timestamp
|
||||
}
|
||||
|
||||
if self.audioInput != nil && self.isStopping, let lastVideoTimestamp = self.lastAudioTimestamp, let lastAudioTimestamp = self.lastAudioTimestamp, lastVideoTimestamp >= lastAudioTimestamp {
|
||||
self.finish()
|
||||
}
|
||||
case .failed:
|
||||
self.isRecording = false
|
||||
self.completion(.writeError(.generic))
|
||||
default:
|
||||
break
|
||||
let type = CMFormatDescriptionGetMediaType(formatDescriptor)
|
||||
if type == kCMMediaType_Video {
|
||||
self.impl.appendVideoSampleBuffer(sampleBuffer)
|
||||
} else if type == kCMMediaType_Audio {
|
||||
if self.configuration.hasAudio {
|
||||
self.impl.appendAudioSampleBuffer(sampleBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendAudio(sampleBuffer: CMSampleBuffer) {
|
||||
self.queue.async {
|
||||
guard let _ = self.assetWriter, let audioInput = self.audioInput, !self.isStopping && !self.finishedWriting else {
|
||||
return
|
||||
}
|
||||
let timestamp = sampleBuffer.presentationTimestamp
|
||||
|
||||
if let _ = self.firstVideoTimestamp {
|
||||
if !self.pendingAudioBuffers.isEmpty {
|
||||
for buffer in self.pendingAudioBuffers {
|
||||
audioInput.append(buffer)
|
||||
}
|
||||
self.pendingAudioBuffers.removeAll()
|
||||
}
|
||||
|
||||
while !audioInput.isReadyForMoreMediaData {
|
||||
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
|
||||
}
|
||||
|
||||
if audioInput.append(sampleBuffer) {
|
||||
self.lastAudioTimestamp = timestamp
|
||||
}
|
||||
} else {
|
||||
self.pendingAudioBuffers.append(sampleBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var duration: Double? {
|
||||
guard let firstTimestamp = self.firstVideoTimestamp, let lastTimestamp = self.lastVideoTimestamp else {
|
||||
return nil
|
||||
}
|
||||
return (lastTimestamp - firstTimestamp).seconds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2521,25 +2521,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let peer, peer.id == self.context.account.peerId, storyContentState.slice == nil {
|
||||
var cameraTransitionIn: StoryCameraTransitionIn?
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
|
||||
let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
@@ -2550,6 +2538,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
return nil
|
||||
})
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
|
||||
return
|
||||
@@ -4951,6 +4940,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.view.addSubview(ConfettiView(frame: self.view.bounds))
|
||||
}
|
||||
}
|
||||
|
||||
private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator?
|
||||
func storyCameraPanGestureChanged(transitionFraction: CGFloat) {
|
||||
guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else {
|
||||
return
|
||||
}
|
||||
|
||||
let coordinator: StoryCameraTransitionInCoordinator?
|
||||
if let current = self.storyCameraTransitionInCoordinator {
|
||||
coordinator = current
|
||||
} else {
|
||||
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
self.storyCameraTransitionInCoordinator = coordinator
|
||||
}
|
||||
coordinator?.updateTransitionProgress(transitionFraction)
|
||||
}
|
||||
|
||||
func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) {
|
||||
if let coordinator = self.storyCameraTransitionInCoordinator {
|
||||
coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity)
|
||||
self.storyCameraTransitionInCoordinator = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {
|
||||
|
||||
@@ -1082,8 +1082,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
}
|
||||
|
||||
if selectedIndex <= 0 && translation.x > 0.0 {
|
||||
let overscroll = translation.x
|
||||
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||
//let overscroll = translation.x
|
||||
//transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||
transitionFraction = 0.0
|
||||
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
|
||||
}
|
||||
|
||||
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
|
||||
@@ -1160,6 +1163,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -22,6 +22,7 @@ import TelegramPermissionsUI
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import PhoneNumberFormat
|
||||
import LocalizedPeerData
|
||||
|
||||
private let dropDownIcon = { () -> UIImage in
|
||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0)
|
||||
@@ -369,72 +370,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
private extension EnginePeer.IndexName {
|
||||
func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult {
|
||||
switch self {
|
||||
case let .title(lhsTitle, _):
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsTitle.caseInsensitiveCompare(rhsString)
|
||||
case let .personName(lhsFirst, lhsLast, _, _):
|
||||
let lhsString: String
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if lhsFirst.isEmpty {
|
||||
lhsString = lhsLast
|
||||
} else {
|
||||
lhsString = lhsFirst + lhsLast
|
||||
}
|
||||
case .lastFirst:
|
||||
if lhsLast.isEmpty {
|
||||
lhsString = lhsFirst
|
||||
} else {
|
||||
lhsString = lhsLast + lhsFirst
|
||||
}
|
||||
}
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsString.caseInsensitiveCompare(rhsString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] {
|
||||
var entries: [ContactListNodeEntry] = []
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ public protocol ContainableController: AnyObject {
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation)
|
||||
func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize?
|
||||
|
||||
func viewWillAppear(_ animated: Bool)
|
||||
|
||||
@@ -1106,9 +1106,13 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
|
||||
func updateTransform(node: ASDisplayNode, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let transform = CATransform3DMakeAffineTransform(transform)
|
||||
|
||||
if CATransform3DEqualToTransform(node.layer.transform, transform) {
|
||||
if CATransform3DEqualToTransform(layer.transform, transform) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
@@ -1117,19 +1121,19 @@ public extension ContainedViewLayoutTransition {
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.transform = transform
|
||||
layer.transform = transform
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousTransform: CATransform3D
|
||||
if beginWithCurrentState, let presentation = node.layer.presentation() {
|
||||
if beginWithCurrentState, let presentation = layer.presentation() {
|
||||
previousTransform = presentation.transform
|
||||
} else {
|
||||
previousTransform = node.layer.transform
|
||||
previousTransform = layer.transform
|
||||
}
|
||||
node.layer.transform = transform
|
||||
node.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
|
||||
layer.transform = transform
|
||||
layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
|
||||
completion?(value)
|
||||
})
|
||||
}
|
||||
@@ -1262,13 +1266,8 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
let t = node.layer.sublayerTransform
|
||||
func updateSublayerTransformScaleAndOffset(layer: CALayer, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.sublayerTransform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
let currentOffset = CGPoint(x: t.m41 / currentScale, y: t.m42 / currentScale)
|
||||
if abs(currentScale - scale) <= CGFloat.ulpOfOne && abs(currentOffset.x - offset.x) <= CGFloat.ulpOfOne && abs(currentOffset.y - offset.y) <= CGFloat.ulpOfOne {
|
||||
@@ -1282,21 +1281,21 @@ public extension ContainedViewLayoutTransition {
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
node.layer.sublayerTransform = transform
|
||||
layer.removeAnimation(forKey: "sublayerTransform")
|
||||
layer.sublayerTransform = transform
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let initialTransform: CATransform3D
|
||||
if beginWithCurrentState, node.isNodeLoaded {
|
||||
initialTransform = node.layer.presentation()?.sublayerTransform ?? t
|
||||
if beginWithCurrentState {
|
||||
initialTransform = layer.presentation()?.sublayerTransform ?? t
|
||||
} else {
|
||||
initialTransform = t
|
||||
}
|
||||
|
||||
node.layer.sublayerTransform = transform
|
||||
node.layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
layer.sublayerTransform = transform
|
||||
layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
@@ -1305,6 +1304,15 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
return updateSublayerTransformScaleAndOffset(layer: node.layer, scale: scale, offset: offset, beginWithCurrentState: beginWithCurrentState, completion: completion)
|
||||
}
|
||||
|
||||
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
|
||||
@@ -185,13 +185,15 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return 39.0
|
||||
case .iPhoneXr:
|
||||
return 41.0 + UIScreenPixel
|
||||
return 41.5
|
||||
case .iPhone12Mini:
|
||||
return 44.0
|
||||
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed:
|
||||
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14ProZoomed:
|
||||
return 47.0 + UIScreenPixel
|
||||
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
||||
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMaxZoomed:
|
||||
return 53.0 + UIScreenPixel
|
||||
case .iPhone14Pro, .iPhone14ProMax:
|
||||
return 55.0
|
||||
case let .unknown(_, _, onScreenNavigationHeight):
|
||||
if let _ = onScreenNavigationHeight {
|
||||
return 39.0
|
||||
|
||||
@@ -566,7 +566,7 @@ public class DrawingContext {
|
||||
f(self.context)
|
||||
}
|
||||
|
||||
public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) {
|
||||
public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil, colorSpace: CGColorSpace? = nil) {
|
||||
if size.width <= 0.0 || size.height <= 0.0 {
|
||||
return nil
|
||||
}
|
||||
@@ -601,7 +601,7 @@ public class DrawingContext {
|
||||
height: Int(self.scaledSize.height),
|
||||
bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent,
|
||||
bytesPerRow: self.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: self.bitmapInfo.rawValue,
|
||||
releaseCallback: nil,
|
||||
releaseInfo: nil
|
||||
@@ -616,7 +616,7 @@ public class DrawingContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func generateImage() -> UIImage? {
|
||||
public func generateImage(colorSpace: CGColorSpace? = nil) -> UIImage? {
|
||||
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
|
||||
return nil
|
||||
}
|
||||
@@ -633,7 +633,7 @@ public class DrawingContext {
|
||||
bitsPerComponent: self.context.bitsPerComponent,
|
||||
bitsPerPixel: self.context.bitsPerPixel,
|
||||
bytesPerRow: self.context.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: self.context.bitmapInfo,
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
|
||||
@@ -1274,9 +1274,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self.filterController(controller, animated: false)
|
||||
}
|
||||
|
||||
public func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
private func scrollToTop(_ subject: NavigationSplitContainerScrollToTop) {
|
||||
if let _ = self.inCallStatusBar {
|
||||
self.inCallNavigate?()
|
||||
@@ -1765,4 +1762,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
private func notifyAccessibilityScreenChanged() {
|
||||
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
|
||||
}
|
||||
|
||||
public func updateRootContainerTransitionOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let rootContainer = self.rootContainer, case let .flat(container) = rootContainer else {
|
||||
return
|
||||
}
|
||||
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +166,12 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
|
||||
public private(set) var modalStyleOverlayTransitionFactor: CGFloat = 0.0
|
||||
public var modalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public var customModalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public func updateModalStyleOverlayTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.modalStyleOverlayTransitionFactor != value {
|
||||
self.modalStyleOverlayTransitionFactor = value
|
||||
self.modalStyleOverlayTransitionFactorUpdated?(transition)
|
||||
self.customModalStyleOverlayTransitionFactorUpdated?(transition)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,10 +454,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
}
|
||||
}
|
||||
|
||||
open func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
|
||||
open func navigationStackConfigurationUpdated(next: [ViewController]) {
|
||||
}
|
||||
|
||||
|
||||
@@ -937,7 +937,7 @@ final class ColorSpectrumComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate {
|
||||
public final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate {
|
||||
private var validSize: CGSize?
|
||||
private var selectedColor: DrawingColor?
|
||||
|
||||
@@ -950,7 +950,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate {
|
||||
private var circleMaskView = UIView()
|
||||
private let maskCircle = SimpleShapeLayer()
|
||||
|
||||
var selected: (DrawingColor) -> Void = { _ in }
|
||||
public var selected: (DrawingColor) -> Void = { _ in }
|
||||
|
||||
private var bitmapData: UnsafeMutableRawPointer?
|
||||
|
||||
@@ -1048,7 +1048,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate {
|
||||
private var animatingIn = false
|
||||
private var scheduledAnimateOut: (() -> Void)?
|
||||
|
||||
func animateIn() {
|
||||
public func animateIn() {
|
||||
self.animatingIn = true
|
||||
|
||||
Queue.mainQueue().after(0.15) {
|
||||
@@ -1107,7 +1107,7 @@ final class ColorSpectrumPickerView: UIView, UIGestureRecognizerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize {
|
||||
public func updateLayout(size: CGSize, selectedColor: DrawingColor?) -> CGSize {
|
||||
let previousSize = self.validSize
|
||||
|
||||
let imageSize = size
|
||||
@@ -2413,10 +2413,10 @@ private final class ColorPickerSheetComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
class ColorPickerScreen: ViewControllerComponentContainer {
|
||||
public final class ColorPickerScreen: ViewControllerComponentContainer {
|
||||
private var dismissed: () -> Void
|
||||
|
||||
init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) {
|
||||
public init(context: AccountContext, initialColor: DrawingColor, updated: @escaping (DrawingColor) -> Void, openEyedropper: @escaping () -> Void, dismissed: @escaping () -> Void = {}) {
|
||||
self.dismissed = dismissed
|
||||
super.init(context: context, component: ColorPickerSheetComponent(context: context, initialColor: initialColor, updated: updated, openEyedropper: openEyedropper, dismissed: dismissed), navigationBarAppearance: .none)
|
||||
|
||||
|
||||
@@ -51,10 +51,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private let size: CGSize
|
||||
|
||||
weak var drawingView: DrawingView?
|
||||
weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
public weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
private(set) var selectedEntityView: DrawingEntityView?
|
||||
public private(set) var selectedEntityView: DrawingEntityView?
|
||||
|
||||
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
|
||||
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
|
||||
@@ -127,7 +127,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0))
|
||||
}
|
||||
|
||||
var entities: [DrawingEntity] {
|
||||
public var entities: [DrawingEntity] {
|
||||
var entities: [DrawingEntity] = []
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
entities.append(view.entity)
|
||||
@@ -220,7 +220,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return CGSize(width: width, height: width)
|
||||
}
|
||||
|
||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
public func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
let center = self.startPosition(relativeTo: relativeTo)
|
||||
let rotation = self.getEntityInitialRotation()
|
||||
let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
|
||||
@@ -485,7 +485,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
}
|
||||
|
||||
func selectEntity(_ entity: DrawingEntity?) {
|
||||
public func selectEntity(_ entity: DrawingEntity?) {
|
||||
if entity?.isMedia == true {
|
||||
return
|
||||
}
|
||||
@@ -593,10 +593,10 @@ protocol DrawingEntityMediaView: DrawingEntityView {
|
||||
|
||||
public class DrawingEntityView: UIView {
|
||||
let context: AccountContext
|
||||
let entity: DrawingEntity
|
||||
public let entity: DrawingEntity
|
||||
var isTracking = false
|
||||
|
||||
weak var selectionView: DrawingEntitySelectionView?
|
||||
public weak var selectionView: DrawingEntitySelectionView?
|
||||
weak var containerView: DrawingEntitiesView?
|
||||
|
||||
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||
@@ -645,7 +645,7 @@ public class DrawingEntityView: UIView {
|
||||
|
||||
}
|
||||
|
||||
func update(animated: Bool = false) {
|
||||
public func update(animated: Bool = false) {
|
||||
self.updateSelectionView()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
didSet {
|
||||
if let previewView = self.previewView {
|
||||
previewView.isUserInteractionEnabled = false
|
||||
previewView.layer.allowsEdgeAntialiasing = true
|
||||
self.addSubview(previewView)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +89,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
}
|
||||
|
||||
public var updated: (() -> Void)?
|
||||
override func update(animated: Bool) {
|
||||
public override func update(animated: Bool) {
|
||||
self.center = self.mediaEntity.position
|
||||
|
||||
let size = self.mediaEntity.baseSize
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ private func generateGridImage(size: CGSize, light: Bool) -> UIImage? {
|
||||
})
|
||||
}
|
||||
|
||||
final class EyedropperView: UIView {
|
||||
public final class EyedropperView: UIView {
|
||||
private weak var drawingView: DrawingView?
|
||||
|
||||
private let containerView: UIView
|
||||
|
||||
@@ -201,7 +201,7 @@ private final class StickerSelectionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
class StickerPickerScreen: ViewController {
|
||||
public class StickerPickerScreen: ViewController {
|
||||
final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private var presentationData: PresentationData
|
||||
private weak var controller: StickerPickerScreen?
|
||||
@@ -999,9 +999,9 @@ class StickerPickerScreen: ViewController {
|
||||
public var pushController: (ViewController) -> Void = { _ in }
|
||||
public var presentController: (ViewController) -> Void = { _ in }
|
||||
|
||||
var completion: (TelegramMediaFile?) -> Void = { _ in }
|
||||
public var completion: (TelegramMediaFile?) -> Void = { _ in }
|
||||
|
||||
init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>) {
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>) {
|
||||
self.context = context
|
||||
self.theme = defaultDarkColorPresentationTheme
|
||||
self.inputData = inputData
|
||||
@@ -1015,14 +1015,14 @@ class StickerPickerScreen: ViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
public override func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
|
||||
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
self.view.endEditing(true)
|
||||
if flag {
|
||||
self.node.animateOut(completion: {
|
||||
@@ -1035,19 +1035,19 @@ class StickerPickerScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: true)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
public override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: false)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = layout
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
||||
let interfaceController: TGPhotoDrawingInterfaceController
|
||||
|
||||
init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) {
|
||||
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView)
|
||||
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView, selectionContainerView: nil)
|
||||
self.interfaceController = interfaceController
|
||||
self.drawingView = interfaceController.drawingView
|
||||
self.drawingEntitiesView = interfaceController.entitiesView
|
||||
|
||||
@@ -59,3 +59,69 @@ public extension EnginePeer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnginePeer.IndexName {
|
||||
func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult {
|
||||
switch self {
|
||||
case let .title(lhsTitle, _):
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsTitle.caseInsensitiveCompare(rhsString)
|
||||
case let .personName(lhsFirst, lhsLast, _, _):
|
||||
let lhsString: String
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if lhsFirst.isEmpty {
|
||||
lhsString = lhsLast
|
||||
} else {
|
||||
lhsString = lhsFirst + lhsLast
|
||||
}
|
||||
case .lastFirst:
|
||||
if lhsLast.isEmpty {
|
||||
lhsString = lhsFirst
|
||||
} else {
|
||||
lhsString = lhsLast + lhsFirst
|
||||
}
|
||||
}
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsString.caseInsensitiveCompare(rhsString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,30 +29,32 @@ final class MediaPickerGridItem: GridItem {
|
||||
let theme: PresentationTheme
|
||||
let selectable: Bool
|
||||
let enableAnimations: Bool
|
||||
let stories: Bool
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.content = content
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
self.enableAnimations = enableAnimations
|
||||
self.stories = stories
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
switch self.content {
|
||||
case let .asset(fetchResult, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
case let .media(media, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
case let .draft(draft, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
}
|
||||
}
|
||||
@@ -64,11 +66,11 @@ final class MediaPickerGridItem: GridItem {
|
||||
}
|
||||
switch self.content {
|
||||
case let .asset(fetchResult, index):
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
case let .media(media, index):
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
case let .draft(draft, index):
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +153,11 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
var identifier: String {
|
||||
return self.selectableItem?.uniqueIdentifier ?? ""
|
||||
if let (draft, _) = self.currentDraftState {
|
||||
return draft.path
|
||||
} else {
|
||||
return self.selectableItem?.uniqueIdentifier ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
var selectableItem: TGMediaSelectableItem? {
|
||||
@@ -235,7 +241,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@@ -262,7 +268,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.updateHiddenMedia()
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@@ -279,7 +285,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.updateHiddenMedia()
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@@ -315,7 +321,12 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
let targetSize: CGSize
|
||||
if stories {
|
||||
targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale)
|
||||
} else {
|
||||
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
}
|
||||
|
||||
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|
||||
|> then(
|
||||
@@ -456,10 +467,18 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func transitionView() -> UIView {
|
||||
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
|
||||
view.frame = self.convert(self.bounds, to: nil)
|
||||
return view
|
||||
func transitionView(snapshot: Bool) -> UIView {
|
||||
if snapshot {
|
||||
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
|
||||
view.frame = self.convert(self.bounds, to: nil)
|
||||
return view
|
||||
} else {
|
||||
return self.imageNode.view
|
||||
}
|
||||
}
|
||||
|
||||
func transitionImage() -> UIImage? {
|
||||
return self.imageNode.image
|
||||
}
|
||||
|
||||
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
|
||||
@@ -53,13 +53,14 @@ private struct MediaPickerGridEntry: Comparable, Identifiable {
|
||||
let stableId: Int
|
||||
let content: MediaPickerGridItemContent
|
||||
let selectable: Bool
|
||||
let stories: Bool
|
||||
|
||||
static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem {
|
||||
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,9 +584,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var updateLayout = false
|
||||
|
||||
var stories = false
|
||||
var selectable = true
|
||||
if case let .assets(_, mode) = controller.subject, mode != .default {
|
||||
selectable = false
|
||||
if mode == .story {
|
||||
stories = true
|
||||
}
|
||||
}
|
||||
|
||||
switch state {
|
||||
@@ -606,7 +611,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var draftIndex = 0
|
||||
for draft in drafts {
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable, stories: stories))
|
||||
stableId += 1
|
||||
draftIndex += 1
|
||||
}
|
||||
@@ -618,7 +623,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
} else {
|
||||
index = totalCount - i - 1
|
||||
}
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable, stories: stories))
|
||||
stableId += 1
|
||||
}
|
||||
|
||||
@@ -647,7 +652,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
case let .media(media):
|
||||
let count = media.count
|
||||
for i in 0 ..< count {
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true, stories: stories))
|
||||
stableId += 1
|
||||
}
|
||||
}
|
||||
@@ -976,7 +981,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
private func transitionView(for identifier: String, hideSource: Bool = false) -> UIView? {
|
||||
fileprivate func transitionView(for identifier: String, snapshot: Bool = true, hideSource: Bool = false) -> UIView? {
|
||||
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
|
||||
return selectionNode.transitionView(for: identifier, hideSource: hideSource)
|
||||
} else {
|
||||
@@ -986,7 +991,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
transitionNode = itemNode
|
||||
}
|
||||
}
|
||||
let transitionView = transitionNode?.transitionView()
|
||||
let transitionView = transitionNode?.transitionView(snapshot: snapshot)
|
||||
if hideSource {
|
||||
transitionNode?.isHidden = true
|
||||
}
|
||||
@@ -994,6 +999,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func transitionImage(for identifier: String) -> UIImage? {
|
||||
var transitionNode: MediaPickerGridItemNode?
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier {
|
||||
transitionNode = itemNode
|
||||
}
|
||||
}
|
||||
return transitionNode?.transitionImage()
|
||||
}
|
||||
|
||||
private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) {
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
|
||||
@@ -1189,7 +1204,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var itemHeight = itemWidth
|
||||
if case let .assets(_, mode) = controller.subject, case .story = mode {
|
||||
itemHeight = 180.0
|
||||
itemHeight = round(itemWidth / 9.0 * 16.0)
|
||||
}
|
||||
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemHeight * 3.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
|
||||
@@ -1905,6 +1920,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
|
||||
return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource)
|
||||
}
|
||||
|
||||
fileprivate func transitionImage(for identifier: String) -> UIImage? {
|
||||
return self.controllerNode.transitionImage(for: identifier)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
@@ -2112,7 +2135,8 @@ public func wallpaperMediaPickerController(
|
||||
|
||||
public func storyMediaPickerController(
|
||||
context: AccountContext,
|
||||
completion: @escaping (Any) -> Void = { _ in }
|
||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void = { _, _, _, _, _ in },
|
||||
dismissed: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
|
||||
@@ -2121,9 +2145,37 @@ public func storyMediaPickerController(
|
||||
})
|
||||
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 = completion
|
||||
mediaPickerController.customSelection = { [weak mediaPickerController] result in
|
||||
guard let controller = mediaPickerController else {
|
||||
return
|
||||
}
|
||||
if let result = result as? MediaEditorDraft {
|
||||
if let transitionView = controller.transitionView(for: result.path, snapshot: false) {
|
||||
let transitionOut: () -> (UIView, CGRect)? = {
|
||||
if let transitionView = controller.transitionView(for: result.path, snapshot: false) {
|
||||
return (transitionView, transitionView.bounds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.path), transitionOut)
|
||||
}
|
||||
} else if let result = result as? PHAsset {
|
||||
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
|
||||
let transitionOut: () -> (UIView, CGRect)? = {
|
||||
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
|
||||
return (transitionView, transitionView.bounds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
present(mediaPickerController, mediaPickerController.mediaPickerContext)
|
||||
}
|
||||
controller.willDismiss = {
|
||||
dismissed()
|
||||
}
|
||||
controller.navigationPresentation = .flatModal
|
||||
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
return controller
|
||||
|
||||
@@ -328,8 +328,8 @@ public func chatMessageLegacySticker(account: Account, userLocation: MediaResour
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad)
|
||||
public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad, colorSpace: colorSpace)
|
||||
}
|
||||
|
||||
public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
@@ -385,7 +385,7 @@ public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaRes
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal: Signal<Tuple3<Data?, Data?, Bool>, NoError>
|
||||
if thumbnail {
|
||||
signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad)
|
||||
@@ -408,7 +408,7 @@ public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUser
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) else {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil, colorSpace: colorSpace) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ extension TelegramUser {
|
||||
if (flags & (1 << 28)) != 0 {
|
||||
userFlags.insert(.isPremium)
|
||||
}
|
||||
if (flags2 & (1 << 2)) != 0 {
|
||||
userFlags.insert(.isCloseFriend)
|
||||
}
|
||||
|
||||
var botInfo: BotUserInfo?
|
||||
if (flags & (1 << 14)) != 0 {
|
||||
@@ -96,7 +99,7 @@ extension TelegramUser {
|
||||
|
||||
static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? {
|
||||
switch rhs {
|
||||
case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _):
|
||||
case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _):
|
||||
let isMin = (flags & (1 << 20)) != 0
|
||||
if !isMin {
|
||||
return TelegramUser(user: rhs)
|
||||
@@ -129,6 +132,9 @@ extension TelegramUser {
|
||||
if (flags & (1 << 28)) != 0 {
|
||||
userFlags.insert(.isPremium)
|
||||
}
|
||||
if (flags2 & (1 << 2)) != 0 {
|
||||
userFlags.insert(.isCloseFriend)
|
||||
}
|
||||
|
||||
var botInfo: BotUserInfo?
|
||||
if (flags & (1 << 14)) != 0 {
|
||||
|
||||
@@ -16,6 +16,7 @@ public struct UserInfoFlags: OptionSet {
|
||||
public static let isScam = UserInfoFlags(rawValue: (1 << 2))
|
||||
public static let isFake = UserInfoFlags(rawValue: (1 << 3))
|
||||
public static let isPremium = UserInfoFlags(rawValue: (1 << 4))
|
||||
public static let isCloseFriend = UserInfoFlags(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
public struct BotUserInfoFlags: OptionSet {
|
||||
@@ -351,4 +352,8 @@ public final class TelegramUser: Peer, Equatable {
|
||||
public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser {
|
||||
return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames)
|
||||
}
|
||||
|
||||
public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser {
|
||||
return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
public func _internal_updateCloseFriends(account: Account, peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
|
||||
let ids: [Int64] = peerIds.map { $0.id._internalGetInt64Value() }
|
||||
return account.network.request(Api.functions.contacts.editCloseFriends(id: ids))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
return account.postbox.transaction { transaction in
|
||||
let contactPeerIds = transaction.getContactPeerIds()
|
||||
var updatedPeers: [Peer] = []
|
||||
for peerId in contactPeerIds {
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramUser {
|
||||
if peerIds.contains(peerId) {
|
||||
var updatedFlags = peer.flags
|
||||
updatedFlags.insert(.isCloseFriend)
|
||||
let updatedPeer = peer.withUpdatedFlags(updatedFlags)
|
||||
updatedPeers.append(updatedPeer)
|
||||
} else if peer.flags.contains(.isCloseFriend) {
|
||||
var updatedFlags = peer.flags
|
||||
updatedFlags.remove(.isCloseFriend)
|
||||
let updatedPeer = peer.withUpdatedFlags(updatedFlags)
|
||||
updatedPeers.append(updatedPeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: updatedPeers, update: { _, updated in
|
||||
return updated
|
||||
})
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
@@ -44,5 +44,9 @@ public extension TelegramEngine {
|
||||
public func updateSelectiveAccountPrivacySettings(type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal<Void, NoError> {
|
||||
return _internal_updateSelectiveAccountPrivacySettings(account: self.account, type: type, settings: settings)
|
||||
}
|
||||
|
||||
public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
|
||||
return _internal_updateCloseFriends(account: self.account, peerIds: peerIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
],
|
||||
|
||||
@@ -3,14 +3,14 @@ import UIKit
|
||||
import ComponentFlow
|
||||
|
||||
final class CameraButton: Component {
|
||||
let content: AnyComponent<Empty>
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let minSize: CGSize?
|
||||
let tag: AnyObject?
|
||||
let isEnabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
content: AnyComponent<Empty>,
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
minSize: CGSize? = nil,
|
||||
tag: AnyObject? = nil,
|
||||
isEnabled: Bool = true,
|
||||
@@ -50,7 +50,7 @@ final class CameraButton: Component {
|
||||
}
|
||||
|
||||
final class View: UIButton, ComponentTaggedView {
|
||||
private let contentView: ComponentHostView<Empty>
|
||||
private var contentView: ComponentHostView<Empty>
|
||||
|
||||
private var component: CameraButton?
|
||||
private var currentIsHighlighted: Bool = false {
|
||||
@@ -123,9 +123,17 @@ final class CameraButton: Component {
|
||||
}
|
||||
|
||||
func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if let currentId = self.component?.content.id, currentId != component.content.id {
|
||||
self.contentView.removeFromSuperview()
|
||||
|
||||
self.contentView = ComponentHostView<Empty>()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
let contentSize = self.contentView.update(
|
||||
transition: transition,
|
||||
component: component.content,
|
||||
component: component.content.component,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import Photos
|
||||
import LottieAnimationComponent
|
||||
import TooltipUI
|
||||
import MediaEditor
|
||||
import BundleIconComponent
|
||||
|
||||
let videoRedColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
@@ -33,29 +34,31 @@ private struct CameraState {
|
||||
}
|
||||
let mode: CameraMode
|
||||
let flashMode: Camera.FlashMode
|
||||
let flashModeDidChange: Bool
|
||||
let recording: Recording
|
||||
let duration: Double
|
||||
|
||||
func updatedMode(_ mode: CameraMode) -> CameraState {
|
||||
return CameraState(mode: mode, flashMode: self.flashMode, recording: self.recording, duration: self.duration)
|
||||
return CameraState(mode: mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: flashMode, recording: self.recording, duration: self.duration)
|
||||
return CameraState(mode: self.mode, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedRecording(_ recording: Recording) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: recording, duration: self.duration)
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedDuration(_ duration: Double) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: self.recording, duration: duration)
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
enum CameraScreenTransition {
|
||||
case animateIn
|
||||
case animateOut
|
||||
case finishedAnimateIn
|
||||
}
|
||||
|
||||
private let cancelButtonTag = GenericComponentViewTag()
|
||||
@@ -71,7 +74,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let context: AccountContext
|
||||
let camera: Camera
|
||||
let changeMode: ActionSlot<CameraMode>
|
||||
let isDismissing: Bool
|
||||
let hasAppeared: Bool
|
||||
let present: (ViewController) -> Void
|
||||
let push: (ViewController) -> Void
|
||||
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@@ -80,7 +83,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
context: AccountContext,
|
||||
camera: Camera,
|
||||
changeMode: ActionSlot<CameraMode>,
|
||||
isDismissing: Bool,
|
||||
hasAppeared: Bool,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
push: @escaping (ViewController) -> Void,
|
||||
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@@ -88,7 +91,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.context = context
|
||||
self.camera = camera
|
||||
self.changeMode = changeMode
|
||||
self.isDismissing = isDismissing
|
||||
self.hasAppeared = hasAppeared
|
||||
self.present = present
|
||||
self.push = push
|
||||
self.completion = completion
|
||||
@@ -98,7 +101,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.isDismissing != rhs.isDismissing {
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -108,7 +111,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
enum ImageKey: Hashable {
|
||||
case cancel
|
||||
case flip
|
||||
case flash
|
||||
}
|
||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||
func image(_ key: ImageKey) -> UIImage {
|
||||
@@ -121,8 +123,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
image = UIImage(bundleImageName: "Camera/CloseIcon")!
|
||||
case .flip:
|
||||
image = UIImage(bundleImageName: "Camera/FlipIcon")!
|
||||
case .flash:
|
||||
image = UIImage(bundleImageName: "Camera/FlashIcon")!
|
||||
}
|
||||
cachedImages[key] = image
|
||||
return image
|
||||
@@ -141,7 +141,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
fileprivate var lastGalleryAsset: PHAsset?
|
||||
private var lastGalleryAssetsDisposable: Disposable?
|
||||
|
||||
var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0)
|
||||
var cameraState = CameraState(mode: .photo, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0)
|
||||
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
||||
|
||||
init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot<Signal<CameraScreen.Result, NoError>>) {
|
||||
@@ -188,6 +188,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
||||
guard hint != self.swipeHint else {
|
||||
return
|
||||
}
|
||||
self.swipeHint = hint
|
||||
self.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
@@ -274,10 +277,15 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if case .none = state.cameraState.recording {
|
||||
let cancelButton = cancelButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponent(Image(
|
||||
image: state.image(.cancel),
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "cancel",
|
||||
component: AnyComponent(
|
||||
Image(
|
||||
image: state.image(.cancel),
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
guard let controller = controller() as? CameraScreen else {
|
||||
return
|
||||
@@ -294,32 +302,50 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
.disappear(.default(scale: true))
|
||||
.cornerRadius(20.0)
|
||||
)
|
||||
|
||||
let flashIconName: String
|
||||
switch state.cameraState.flashMode {
|
||||
case .off:
|
||||
flashIconName = "flash_off"
|
||||
case .on:
|
||||
flashIconName = "flash_on"
|
||||
case .auto:
|
||||
flashIconName = "flash_auto"
|
||||
@unknown default:
|
||||
flashIconName = "flash_off"
|
||||
}
|
||||
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponent(
|
||||
|
||||
let flashContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
let flashIconName: String
|
||||
switch state.cameraState.flashMode {
|
||||
case .off:
|
||||
flashIconName = "flash_off"
|
||||
case .on:
|
||||
flashIconName = "flash_on"
|
||||
case .auto:
|
||||
flashIconName = "flash_auto"
|
||||
@unknown default:
|
||||
flashIconName = "flash_off"
|
||||
}
|
||||
|
||||
flashContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: flashIconName,
|
||||
mode: .animating(loop: false),
|
||||
mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
|
||||
range: nil
|
||||
),
|
||||
colors: [:],
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
flashContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Camera/FlashOffIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: flashContentComponent,
|
||||
action: { [weak state] in
|
||||
guard let state else {
|
||||
return
|
||||
@@ -514,7 +540,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if case .none = state.cameraState.recording, !component.isDismissing {
|
||||
if case .none = state.cameraState.recording {
|
||||
let modeControl = modeControl.update(
|
||||
component: ModeComponent(
|
||||
availableModes: [.photo, .video],
|
||||
@@ -638,13 +664,16 @@ public class CameraScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
private let updateState: ActionSlot<CameraState>
|
||||
|
||||
private let backgroundEffectView: UIVisualEffectView
|
||||
private let backgroundDimView: UIView
|
||||
fileprivate let backgroundView: UIView
|
||||
fileprivate let containerView: UIView
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
private let previewContainerView: UIView
|
||||
fileprivate let previewView: CameraPreviewView?
|
||||
fileprivate let simplePreviewView: CameraSimplePreviewView?
|
||||
fileprivate let previewBlurView: BlurView
|
||||
private var previewSnapshotView: UIView?
|
||||
fileprivate let transitionDimView: UIView
|
||||
fileprivate let transitionCornersView: UIImageView
|
||||
fileprivate let camera: Camera
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@@ -665,7 +694,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var previewBlurPromise = ValuePromise<Bool>(false)
|
||||
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
|
||||
|
||||
init(controller: CameraScreen) {
|
||||
self.controller = controller
|
||||
@@ -674,9 +703,11 @@ public class CameraScreen: ViewController {
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.backgroundEffectView = UIVisualEffectView(effect: nil)
|
||||
self.backgroundDimView = UIView()
|
||||
self.backgroundDimView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
self.backgroundView = UIView()
|
||||
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
|
||||
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
|
||||
|
||||
@@ -710,17 +741,25 @@ public class CameraScreen: ViewController {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
self.transitionDimView = UIView()
|
||||
self.transitionDimView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
self.transitionDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.transitionCornersView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.view.addSubview(self.backgroundEffectView)
|
||||
self.view.addSubview(self.backgroundDimView)
|
||||
self.view.addSubview(self.backgroundView)
|
||||
self.view.addSubview(self.containerView)
|
||||
|
||||
self.view.addSubview(self.previewContainerView)
|
||||
self.containerView.addSubview(self.previewContainerView)
|
||||
self.previewContainerView.addSubview(self.effectivePreviewView)
|
||||
self.previewContainerView.addSubview(self.previewBlurView)
|
||||
self.containerView.addSubview(self.transitionDimView)
|
||||
self.view.addSubview(self.transitionCornersView)
|
||||
|
||||
self.changingPositionDisposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
@@ -733,7 +772,9 @@ public class CameraScreen: ViewController {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
})
|
||||
} else if forceBlur {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
}
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.previewBlurView.effect = nil
|
||||
@@ -776,7 +817,8 @@ public class CameraScreen: ViewController {
|
||||
self.camera.stopCapture()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -798,7 +840,10 @@ public class CameraScreen: ViewController {
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer)
|
||||
|
||||
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5))
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
|
||||
self.camera.startCapture()
|
||||
}
|
||||
|
||||
@@ -814,96 +859,49 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var panTranslation: CGFloat?
|
||||
private var previewInitialPosition: CGPoint?
|
||||
private var controlsInitialPosition: CGPoint?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.panTranslation = nil
|
||||
self.previewInitialPosition = self.previewContainerView.center
|
||||
self.controlsInitialPosition = self.componentHost.view?.center
|
||||
break
|
||||
case .changed:
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
if !"".isEmpty {
|
||||
|
||||
} else {
|
||||
if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil {
|
||||
self.changeMode.invoke(translation.x > 0.0 ? .photo : .video)
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
} else if translation.y > 10.0 {
|
||||
let isFirstPanChange = self.panTranslation == nil
|
||||
self.panTranslation = translation.y
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y)
|
||||
}
|
||||
|
||||
if self.backgroundEffectView.isHidden {
|
||||
self.backgroundEffectView.isHidden = false
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = nil
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
})
|
||||
}
|
||||
|
||||
if isFirstPanChange {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
if translation.x < -10.0 {
|
||||
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
|
||||
controller.updateTransitionProgress(transitionFraction, transition: .immediate)
|
||||
} else if translation.y < -10.0 {
|
||||
self.controller?.presentGallery()
|
||||
controller.presentGallery()
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||
if velocity.y > 1000.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else {
|
||||
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
transition.setPosition(view: self.previewContainerView, position: previewInitialPosition)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
transition.setPosition(view: view, position: controlsInitialPosition)
|
||||
}
|
||||
if !self.backgroundEffectView.isHidden {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
}, completion: { _ in
|
||||
self.backgroundEffectView.isHidden = true
|
||||
})
|
||||
}
|
||||
}
|
||||
if let _ = self.panTranslation {
|
||||
self.panTranslation = nil
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
|
||||
controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let previewView = self.simplePreviewView else {
|
||||
return
|
||||
}
|
||||
let location = gestureRecognizer.location(in: previewView)
|
||||
let point = previewView.cameraPoint(for: location)
|
||||
self.camera.focus(at: point, autoFocus: false)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
|
||||
}, completion: { _ in
|
||||
self.backgroundEffectView.isHidden = true
|
||||
self.backgroundView.alpha = 1.0
|
||||
})
|
||||
|
||||
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
|
||||
@@ -926,23 +924,13 @@ public class CameraScreen: ViewController {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||
view.layer.shadowRadius = 4.0
|
||||
view.layer.shadowColor = UIColor.black.cgColor
|
||||
view.layer.shadowOpacity = 0.2
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.camera.stopCapture(invalidate: true)
|
||||
|
||||
self.backgroundEffectView.isHidden = false
|
||||
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = nil
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
})
|
||||
|
||||
if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView {
|
||||
@@ -994,55 +982,79 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func commitTransitionToEditor() {
|
||||
self.previewContainerView.alpha = 0.0
|
||||
func pauseCameraCapture() {
|
||||
self.simplePreviewView?.isEnabled = false
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.previewBlurPromise.set(true)
|
||||
}
|
||||
self.camera.stopCapture()
|
||||
}
|
||||
|
||||
private var previewSnapshotView: UIView?
|
||||
func animateInFromEditor() {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) {
|
||||
self.simplePreviewView?.addSubview(snapshot)
|
||||
self.previewSnapshotView = snapshot
|
||||
}
|
||||
self.simplePreviewView?.isEnabled = true
|
||||
self.camera.startCapture()
|
||||
|
||||
if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing {
|
||||
let _ = (isPreviewing
|
||||
|> filter {
|
||||
$0
|
||||
func resumeCameraCapture() {
|
||||
if self.simplePreviewView?.isEnabled == false {
|
||||
if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) {
|
||||
self.simplePreviewView?.addSubview(snapshot)
|
||||
self.previewSnapshotView = snapshot
|
||||
}
|
||||
|> take(1)).start(next: { [weak self] _ in
|
||||
if let self {
|
||||
self.simplePreviewView?.isEnabled = true
|
||||
self.camera.startCapture()
|
||||
|
||||
if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing {
|
||||
let _ = (isPreviewing
|
||||
|> filter {
|
||||
$0
|
||||
}
|
||||
|> take(1)).start(next: { [weak self] _ in
|
||||
if let self {
|
||||
self.previewBlurPromise.set(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Queue.mainQueue().after(1.0) {
|
||||
self.previewBlurPromise.set(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Queue.mainQueue().after(1.0) {
|
||||
self.previewBlurPromise.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateInFromEditor(toGallery: Bool) {
|
||||
if !toGallery {
|
||||
self.resumeCameraCapture()
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
let progress = 1.0 - value
|
||||
let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
let maxOffset = -56.0
|
||||
|
||||
let scale = 1.0 * progress + (1.0 - progress) * maxScale
|
||||
let offset = (1.0 - progress) * maxOffset
|
||||
transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true)
|
||||
}
|
||||
|
||||
func presentDraftTooltip() {
|
||||
@@ -1067,8 +1079,22 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func requestUpdateLayout(hasAppeared: Bool, transition: Transition) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, forceUpdate: true, hasAppeared: hasAppeared, transition: transition)
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||
view.layer.shadowRadius = 4.0
|
||||
view.layer.shadowColor = UIColor.black.cgColor
|
||||
view.layer.shadowOpacity = 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
|
||||
fileprivate var hasAppeared = false
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
|
||||
guard let _ = self.controller else {
|
||||
return
|
||||
}
|
||||
@@ -1103,8 +1129,9 @@ public class CameraScreen: ViewController {
|
||||
var transition = transition
|
||||
if isFirstTime {
|
||||
transition = transition.withUserData(CameraScreenTransition.animateIn)
|
||||
} else if animateOut {
|
||||
transition = transition.withUserData(CameraScreenTransition.animateOut)
|
||||
} else if hasAppeared && !self.hasAppeared {
|
||||
self.hasAppeared = hasAppeared
|
||||
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
||||
}
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
@@ -1114,7 +1141,7 @@ public class CameraScreen: ViewController {
|
||||
context: self.context,
|
||||
camera: self.camera,
|
||||
changeMode: self.changeMode,
|
||||
isDismissing: self.panTranslation != nil,
|
||||
hasAppeared: self.hasAppeared,
|
||||
present: { [weak self] c in
|
||||
self?.controller?.present(c, in: .window(.root))
|
||||
},
|
||||
@@ -1127,34 +1154,48 @@ public class CameraScreen: 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.insertSubview(componentView, belowSubview: transitionDimView)
|
||||
componentView.clipsToBounds = true
|
||||
}
|
||||
|
||||
if self.panTranslation == nil {
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: componentFrame)
|
||||
}
|
||||
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: componentFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.setPosition(view: self.backgroundView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
if self.panTranslation == nil {
|
||||
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
}
|
||||
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))
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
|
||||
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
|
||||
let screenCornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
if screenCornerRadius > 0.0, self.transitionCornersView.image == nil {
|
||||
self.transitionCornersView.image = generateImage(CGSize(width: screenCornerRadius, height: screenCornerRadius * 3.0), rotatedContext: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
context.setBlendMode(.clear)
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: size.width)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(screenCornerRadius))
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1167,15 +1208,37 @@ public class CameraScreen: ViewController {
|
||||
fileprivate let holder: CameraHolder?
|
||||
fileprivate let transitionIn: TransitionIn?
|
||||
fileprivate let transitionOut: (Bool) -> TransitionOut?
|
||||
fileprivate let completion: (Signal<CameraScreen.Result, NoError>) -> Void
|
||||
|
||||
public final class ResultTransition {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceImage: UIImage?
|
||||
public let transitionOut: () -> (UIView, CGRect)?
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceImage: UIImage?,
|
||||
transitionOut: @escaping () -> (UIView, CGRect)?
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceImage = sourceImage
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
}
|
||||
fileprivate let completion: (Signal<CameraScreen.Result, NoError>, ResultTransition?) -> Void
|
||||
public var transitionedIn: () -> Void = {}
|
||||
|
||||
private var audioSessionDisposable: Disposable?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
mode: Mode,
|
||||
holder: CameraHolder? = nil,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (Signal<CameraScreen.Result, NoError>) -> Void
|
||||
completion: @escaping (Signal<CameraScreen.Result, NoError>, ResultTransition?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@@ -1186,15 +1249,21 @@ public class CameraScreen: ViewController {
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.requestAudioSession()
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.audioSessionDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
@@ -1202,31 +1271,61 @@ public class CameraScreen: ViewController {
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
public func returnFromEditor() {
|
||||
self.node.animateInFromEditor()
|
||||
private func requestAudioSession() {
|
||||
self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in }, deactivate: { _ in
|
||||
return .single(Void())
|
||||
})
|
||||
}
|
||||
|
||||
public func commitTransitionToEditor() {
|
||||
self.node.commitTransitionToEditor()
|
||||
private weak var galleryController: ViewController?
|
||||
public func returnFromEditor() {
|
||||
self.node.animateInFromEditor(toGallery: self.galleryController != nil)
|
||||
}
|
||||
|
||||
func presentGallery() {
|
||||
var dismissGalleryControllerImpl: (() -> Void)?
|
||||
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result in
|
||||
dismissGalleryControllerImpl?()
|
||||
var didStopCameraCapture = false
|
||||
let stopCameraCapture = { [weak self] in
|
||||
guard !didStopCameraCapture, let self else {
|
||||
return
|
||||
}
|
||||
didStopCameraCapture = true
|
||||
|
||||
self.node.pauseCameraCapture()
|
||||
}
|
||||
|
||||
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut in
|
||||
if let self {
|
||||
self.node.animateOutToEditor()
|
||||
stopCameraCapture()
|
||||
|
||||
let resultTransition = ResultTransition(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionRect,
|
||||
sourceImage: transitionImage,
|
||||
transitionOut: transitionOut
|
||||
)
|
||||
if let asset = result as? PHAsset {
|
||||
self.completion(.single(.asset(asset)))
|
||||
self.completion(.single(.asset(asset)), resultTransition)
|
||||
} else if let draft = result as? MediaEditorDraft {
|
||||
self.completion(.single(.draft(draft)))
|
||||
self.completion(.single(.draft(draft)), resultTransition)
|
||||
}
|
||||
}
|
||||
}, dismissed: { [weak self] in
|
||||
if let self {
|
||||
self.node.resumeCameraCapture()
|
||||
}
|
||||
})
|
||||
dismissGalleryControllerImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true)
|
||||
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
|
||||
if let self, let controller {
|
||||
let transitionFactor = controller.modalStyleOverlayTransitionFactor
|
||||
if transitionFactor > 0.1 {
|
||||
stopCameraCapture()
|
||||
}
|
||||
self.node.updateModalTransitionFactor(transitionFactor, transition: transition)
|
||||
}
|
||||
}
|
||||
push(controller)
|
||||
|
||||
self.galleryController = controller
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
public func presentDraftTooltip() {
|
||||
@@ -1234,21 +1333,83 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
private var isDismissed = false
|
||||
fileprivate func requestDismiss(animated: Bool) {
|
||||
fileprivate func requestDismiss(animated: Bool, interactive: Bool = false) {
|
||||
guard !self.isDismissed else {
|
||||
return
|
||||
}
|
||||
self.node.camera.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
if animated {
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
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)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private var isTransitioning = false
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
self.isTransitioning = true
|
||||
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))
|
||||
let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction))
|
||||
transition.updateSublayerTransformScaleAndOffset(layer: self.node.containerView.layer, scale: scale, offset: CGPoint(x: -offsetX * 1.0 / scale * 0.5, y: 0.0), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
let dimAlpha = 0.6 * (1.0 - transitionFraction)
|
||||
transition.updateAlpha(layer: self.node.transitionDimView.layer, alpha: dimAlpha)
|
||||
transition.updateTransform(layer: self.node.transitionCornersView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
|
||||
self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true)
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
let offsetX = floorToScreenPixels(transitionFraction * self.node.frame.width)
|
||||
navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
self.isTransitioning = false
|
||||
if dismissing {
|
||||
if transitionFraction < 0.7 || velocity < -1000.0 {
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
} else {
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if transitionFraction > 0.33 || velocity > 1000.0 {
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
self.node.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
self.transitionedIn()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
if !flag {
|
||||
self.galleryController?.dismiss(animated: false)
|
||||
}
|
||||
super.dismiss(animated: flag, completion: completion)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
||||
+20
-14
@@ -108,7 +108,7 @@ private final class ShutterButtonContentComponent: Component {
|
||||
}
|
||||
self.blobView.updatePrimaryOffset(bandedOffset, transition: transition)
|
||||
} else {
|
||||
self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.15))
|
||||
self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,11 +411,11 @@ final class CaptureControlsComponent: Component {
|
||||
if let galleryButton = self.galleryButtonView.view {
|
||||
blobOffset = galleryButton.center.x - self.frame.width / 2.0
|
||||
}
|
||||
self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.5)))
|
||||
self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.35)))
|
||||
} else {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.component?.shutterReleased()
|
||||
self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.3)))
|
||||
self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.25)))
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -450,7 +450,7 @@ final class CaptureControlsComponent: Component {
|
||||
self.component?.zoomUpdated(1.0)
|
||||
}
|
||||
|
||||
if location.x < self.frame.width / 2.0 - 20.0 {
|
||||
if location.x < self.frame.width / 2.0 - 30.0 {
|
||||
if location.x < self.frame.width / 2.0 - 60.0 {
|
||||
self.component?.swipeHintUpdated(.releaseLock)
|
||||
if location.x < 75.0 {
|
||||
@@ -464,7 +464,7 @@ final class CaptureControlsComponent: Component {
|
||||
blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0)
|
||||
isBanding = true
|
||||
}
|
||||
} else if location.x > self.frame.width / 2.0 + 20.0 {
|
||||
} else if location.x > self.frame.width / 2.0 + 30.0 {
|
||||
self.component?.swipeHintUpdated(.flip)
|
||||
if location.x > self.frame.width / 2.0 + 60.0 {
|
||||
self.panBlobState = .transientToFlip
|
||||
@@ -488,8 +488,8 @@ final class CaptureControlsComponent: Component {
|
||||
}
|
||||
var transition: Transition = .immediate
|
||||
if let wasBanding = self.wasBanding, wasBanding != isBanding {
|
||||
self.hapticFeedback.impact(.light)
|
||||
transition = .spring(duration: 0.3)
|
||||
//self.hapticFeedback.impact(.light)
|
||||
transition = .spring(duration: 0.35)
|
||||
}
|
||||
self.wasBanding = isBanding
|
||||
self.shutterUpdateOffset.invoke((blobOffset, transition))
|
||||
@@ -574,11 +574,14 @@ final class CaptureControlsComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponent(
|
||||
Image(
|
||||
image: state.cachedAssetImage?.1,
|
||||
size: CGSize(width: 50.0, height: 50.0),
|
||||
contentMode: .scaleAspectFill
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "gallery",
|
||||
component: AnyComponent(
|
||||
Image(
|
||||
image: state.cachedAssetImage?.1,
|
||||
size: CGSize(width: 50.0, height: 50.0),
|
||||
contentMode: .scaleAspectFill
|
||||
)
|
||||
)
|
||||
),
|
||||
tag: component.galleryButtonTag,
|
||||
@@ -632,8 +635,11 @@ final class CaptureControlsComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponent(
|
||||
FlipButtonContentComponent(action: flipAnimationAction)
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "flip",
|
||||
component: AnyComponent(
|
||||
FlipButtonContentComponent(action: flipAnimationAction)
|
||||
)
|
||||
),
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class FocusCrosshairsView: UIView {
|
||||
private let indicatorView: UIImageView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.indicatorView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.indicatorView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(pointOfInterest: CGPoint) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -41,15 +41,15 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
|
||||
func update(value: T, transition: Transition = .immediate) {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if case .none = transition.animation {
|
||||
if let animation = self.animation, case let .curve(duration, curve) = animation.animation {
|
||||
self.value = value
|
||||
let elapsed = duration - (CACurrentMediaTime() - animation.startTimestamp)
|
||||
if elapsed < 0.1 {
|
||||
self.presentationValue = value
|
||||
self.animation = nil
|
||||
let elapsed = duration - (currentTimestamp - animation.startTimestamp)
|
||||
if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp)
|
||||
} else {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: CACurrentMediaTime())
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp)
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
@@ -58,7 +58,7 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: CACurrentMediaTime())
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +73,18 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
case .easeInOut:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
case .spring:
|
||||
t = listViewAnimationCurveSystem(t)
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
//t = listViewAnimationCurveSystem(t)
|
||||
case let .custom(x1, y1, x2, y2):
|
||||
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
|
||||
}
|
||||
self.presentationValue = animation.valueAt(t) as! T
|
||||
|
||||
return timeFromStart <= duration
|
||||
if timeFromStart <= duration {
|
||||
return true
|
||||
}
|
||||
self.animation = nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,26 +209,21 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
super.init(frame: CGRect(), device: device)
|
||||
|
||||
self.delegate = self
|
||||
|
||||
self.isOpaque = false
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.framebufferOnly = true
|
||||
self.colorPixelFormat = .bgra8Unorm
|
||||
self.framebufferOnly = true
|
||||
//self.presentsWithTransaction = true
|
||||
self.isPaused = true
|
||||
self.delegate = self
|
||||
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.tick()
|
||||
}
|
||||
self.displayLink?.isPaused = true
|
||||
|
||||
self.isPaused = true
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
self.viewportDimensions = size
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@@ -232,6 +232,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
self.viewportDimensions = size
|
||||
}
|
||||
|
||||
func updateState(_ state: BlobState, transition: Transition = .immediate) {
|
||||
guard self.state != state else {
|
||||
return
|
||||
@@ -253,6 +257,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.primaryOffset.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
@@ -262,6 +267,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.secondaryOffset.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
@@ -303,32 +309,30 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
let renderPassDescriptor = self.currentRenderPassDescriptor!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
|
||||
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let viewportDimensions = self.viewportDimensions
|
||||
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
|
||||
|
||||
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
|
||||
|
||||
|
||||
let w = Float(1)
|
||||
let h = Float(1)
|
||||
|
||||
var vertices: [Float] = [
|
||||
w, -h,
|
||||
w, -h,
|
||||
-w, -h,
|
||||
-w, h,
|
||||
w, -h,
|
||||
w, -h,
|
||||
-w, h,
|
||||
w, h
|
||||
w, h
|
||||
]
|
||||
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
|
||||
|
||||
|
||||
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
|
||||
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
|
||||
var primaryParameters = simd_float4(
|
||||
Float(self.primarySize.presentationValue),
|
||||
Float(self.primaryOffset.presentationValue),
|
||||
@@ -343,14 +347,11 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
Float(self.secondaryRedness.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float3>.size, index: 2)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
drawable.present()
|
||||
//commandBuffer.present(drawable)
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
commandBuffer.commit()
|
||||
commandBuffer.waitUntilScheduled()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
@@ -120,15 +120,6 @@ final class ZoomComponent: Component {
|
||||
let spacing: CGFloat = 3.0
|
||||
let buttonSize = CGSize(width: 37.0, height: 37.0)
|
||||
let size: CGSize = CGSize(width: buttonSize.width * CGFloat(component.availableValues.count) + spacing * CGFloat(component.availableValues.count - 1) + sideInset * 2.0, height: 43.0)
|
||||
|
||||
if let screenTransition = transition.userData(CameraScreenTransition.self) {
|
||||
switch screenTransition {
|
||||
case .animateIn:
|
||||
self.animateIn()
|
||||
case .animateOut:
|
||||
self.animateOut()
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 3.0), size: buttonSize)
|
||||
|
||||
@@ -21,34 +21,17 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]],
|
||||
}
|
||||
|
||||
fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]],
|
||||
constant float2 &texCoordScales [[buffer(0)]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]]) {
|
||||
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
|
||||
|
||||
float scaleX = texCoordScales.x;
|
||||
float scaleY = texCoordScales.y;
|
||||
float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX;
|
||||
float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY;
|
||||
if (x < 0 || x > 1 || y < 0 || y > 1) {
|
||||
return half4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
half3 color = texture.sample(samplr, float2(x, y)).rgb;
|
||||
half3 color = texture.sample(samplr, in.texCoord).rgb;
|
||||
return half4(color, 1.0);
|
||||
}
|
||||
|
||||
fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]],
|
||||
constant float2 &texCoordScales [[buffer(0)]],
|
||||
texture2d<half, access::sample> texture [[texture(0)]]) {
|
||||
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
|
||||
|
||||
float scaleX = texCoordScales.x;
|
||||
float scaleY = texCoordScales.y;
|
||||
float x = (in.texCoord.x - (1.0 - scaleX) / 2.0) / scaleX;
|
||||
float y = (in.texCoord.y - (1.0 - scaleY) / 2.0) / scaleY;
|
||||
if (x < 0 || x > 1 || y < 0 || y > 1) {
|
||||
return 0.0;
|
||||
}
|
||||
half3 color = texture.sample(samplr, float2(x, y)).rgb;
|
||||
half3 color = texture.sample(samplr, in.texCoord).rgb;
|
||||
half luma = color.r * 0.3 + color.g * 0.59 + color.b * 0.11;
|
||||
return luma;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ fragment half4 enhanceColorLookupFragmentShader(RasterizerData in [[stage_in]],
|
||||
constexpr sampler lutSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero);
|
||||
|
||||
float2 sourceCoord = in.texCoord;
|
||||
half4 color = sourceTexture.sample(colorSampler,sourceCoord);
|
||||
half4 color = sourceTexture.sample(colorSampler, sourceCoord);
|
||||
half3 hslColor = rgbToHsl(color.rgb);
|
||||
|
||||
float txf = sourceCoord.x * tileGridSize.x - 0.5;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <metal_stdlib>
|
||||
#include "EditorCommon.h"
|
||||
#include "EditorUtils.h"
|
||||
|
||||
using namespace metal;
|
||||
|
||||
static inline float4 BT709_decode(const float Y, const float Cb, const float Cr) {
|
||||
float Yn = Y;
|
||||
|
||||
float Cbn = (Cb - (128.0f/255.0f));
|
||||
float Crn = (Cr - (128.0f/255.0f));
|
||||
|
||||
float3 YCbCr = float3(Yn, Cbn, Crn);
|
||||
|
||||
const float3x3 kColorConversion709 = float3x3(float3(1.0, 1.0, 1.0),
|
||||
float3(0.0f, -0.1873, 1.8556),
|
||||
float3(1.5748, -0.4681, 0.0));
|
||||
|
||||
float3 rgb = kColorConversion709 * YCbCr;
|
||||
|
||||
rgb = saturate(rgb);
|
||||
|
||||
return float4(rgb.r, rgb.g, rgb.b, 1.0f);
|
||||
}
|
||||
|
||||
|
||||
fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]],
|
||||
texture2d<half, access::sample> inYTexture [[texture(0)]],
|
||||
texture2d<half, access::sample> inUVTexture [[texture(1)]]
|
||||
)
|
||||
{
|
||||
constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest);
|
||||
|
||||
float Y = float(inYTexture.sample(textureSampler, in.texCoord).r);
|
||||
half2 uvSamples = inUVTexture.sample(textureSampler, in.texCoord).rg;
|
||||
|
||||
float Cb = float(uvSamples[0]);
|
||||
float Cr = float(uvSamples[1]);
|
||||
|
||||
float4 pixel = BT709_decode(Y, Cb, Cr);
|
||||
return pixel;
|
||||
}
|
||||
@@ -18,6 +18,45 @@ struct MediaEditorAdjustments {
|
||||
var warmth: simd_float1
|
||||
var grain: simd_float1
|
||||
var vignette: simd_float1
|
||||
|
||||
var hasValues: Bool {
|
||||
let epsilon: simd_float1 = 0.005
|
||||
|
||||
if abs(self.shadows) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.highlights) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.contrast) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.fade) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.saturation) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.shadowsTintIntensity) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.highlightsTintIntensity) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.exposure) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.warmth) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.grain) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.vignette) > epsilon {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class AdjustmentsRenderPass: DefaultRenderPass {
|
||||
@@ -50,10 +89,14 @@ final class AdjustmentsRenderPass: DefaultRenderPass {
|
||||
return "adjustmentsFragmentShader"
|
||||
}
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.adjustments.hasValues else {
|
||||
return input
|
||||
}
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
self.adjustments.dimensions = simd_float2(Float(width), Float(height))
|
||||
|
||||
@@ -29,11 +29,11 @@ private final class BlurGaussianPass: RenderPass {
|
||||
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, intensity: Float, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, intensity: Float, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
let radius = round(4.0 + intensity * 26.0)
|
||||
if self.blur?.sigma != radius {
|
||||
self.blur = MPSImageGaussianBlur(device: device, sigma: radius)
|
||||
@@ -67,10 +67,8 @@ private final class BlurLinearPass: DefaultRenderPass {
|
||||
return "blurLinearFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
@@ -83,7 +81,7 @@ private final class BlurLinearPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@@ -105,10 +103,8 @@ private final class BlurRadialPass: DefaultRenderPass {
|
||||
return "blurRadialFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
@@ -121,7 +117,7 @@ private final class BlurRadialPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@@ -145,11 +141,9 @@ private final class BlurPortraitPass: DefaultRenderPass {
|
||||
return "blurPortraitFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
@@ -161,7 +155,7 @@ private final class BlurPortraitPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@@ -208,17 +202,18 @@ final class BlurRenderPass: RenderPass {
|
||||
self.portraitPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.process(input: input, maskTexture: self.maskTexture, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.process(input: input, maskTexture: self.maskTexture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, maskTexture: MTLTexture?, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, maskTexture: MTLTexture?, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.intensity > 0.005 && self.mode != .off else {
|
||||
return input
|
||||
}
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
self.value.aspectRatio = Float(height) / Float(width)
|
||||
|
||||
@@ -235,18 +230,18 @@ final class BlurRenderPass: RenderPass {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
|
||||
guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, rotation: rotation, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else {
|
||||
guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else {
|
||||
return input
|
||||
}
|
||||
|
||||
switch self.mode {
|
||||
case .linear:
|
||||
return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
case .radial:
|
||||
return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
case .portrait:
|
||||
if let maskTexture {
|
||||
return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
|
||||
@@ -21,20 +21,12 @@ private final class EnhanceLightnessPass: DefaultRenderPass {
|
||||
return .r8Unorm
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, size: TextureSize, scale: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, size: TextureSize, scale: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees:
|
||||
width = size.height
|
||||
height = size.width
|
||||
default:
|
||||
width = size.width
|
||||
height = size.height
|
||||
}
|
||||
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
@@ -130,7 +122,7 @@ private final class EnhanceLUTGeneratorPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -204,10 +196,11 @@ private final class EnhanceLookupPass: DefaultRenderPass {
|
||||
return "enhanceColorLookupFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
@@ -269,7 +262,7 @@ final class EnhanceRenderPass: RenderPass {
|
||||
self.lookupPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.value > 0.005 else {
|
||||
return input
|
||||
}
|
||||
@@ -279,12 +272,12 @@ final class EnhanceRenderPass: RenderPass {
|
||||
let lightnessSize = TextureSize(width: input.width + dX, height: input.height + dY)
|
||||
let lightnessScale = simd_float2(Float(input.width + dX) / Float(input.width), Float(input.height + dY) / Float(input.height))
|
||||
|
||||
let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
let lookupTexture = self.lutGeneratorPass.process(input: lightness!, gridSize: self.tileGridSize, clipLimit: self.clipLimit, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
let gridSize = simd_float2(Float(self.tileGridSize.width), Float(self.tileGridSize.height))
|
||||
let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
+49
-48
@@ -8,6 +8,7 @@ final class HistogramCalculationPass: DefaultRenderPass {
|
||||
fileprivate var histogramBuffer: MTLBuffer?
|
||||
fileprivate var calculation: MPSImageHistogram?
|
||||
|
||||
var isEnabled = false
|
||||
var updated: ((Data) -> Void)?
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
@@ -40,58 +41,58 @@ final class HistogramCalculationPass: DefaultRenderPass {
|
||||
super.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = .r8Unorm
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
if self.isEnabled {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = .r8Unorm
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "lumaTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "lumaTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0)
|
||||
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength)
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length)
|
||||
self.updated?(histogramData)
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0)
|
||||
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength)
|
||||
|
||||
let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length)
|
||||
self.updated?(histogramData)
|
||||
}
|
||||
}
|
||||
|
||||
return input
|
||||
|
||||
@@ -3,45 +3,57 @@ import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
import Display
|
||||
import Accelerate
|
||||
|
||||
func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? {
|
||||
func dataForImage(_ image: UIImage) -> UnsafeMutablePointer<UInt8> {
|
||||
let imageRef = image.cgImage
|
||||
let width = Int(image.size.width)
|
||||
let height = Int(image.size.height)
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height * 4)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * Int(width)
|
||||
let bitsPerComponent = 8
|
||||
let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
|
||||
let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
|
||||
context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
|
||||
return rawData
|
||||
}
|
||||
|
||||
let width = Int(image.size.width * image.scale)
|
||||
let height = Int(image.size.height * image.scale)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * width
|
||||
|
||||
var texture : MTLTexture?
|
||||
let region = MTLRegionMake2D(0, 0, Int(width), Int(height))
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false)
|
||||
texture = device.makeTexture(descriptor: textureDescriptor)
|
||||
|
||||
let data = dataForImage(image)
|
||||
texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow)
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
final class ImageTextureSource: TextureSource {
|
||||
weak var output: TextureConsumer?
|
||||
|
||||
var textureLoader: MTKTextureLoader?
|
||||
var texture: MTLTexture?
|
||||
|
||||
init(image: UIImage, renderTarget: RenderTarget) {
|
||||
guard let device = renderTarget.mtlDevice, var cgImage = image.cgImage else {
|
||||
return
|
||||
if let device = renderTarget.mtlDevice {
|
||||
self.texture = loadTexture(image: image, device: device)
|
||||
}
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
self.textureLoader = textureLoader
|
||||
|
||||
if let bitsPerPixel = image.cgImage?.bitsPerPixel, bitsPerPixel > 32 {
|
||||
let updatedImage = generateImage(image.size, contextGenerator: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
}, opaque: false)
|
||||
cgImage = updatedImage?.cgImage ?? cgImage
|
||||
}
|
||||
|
||||
self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false])
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
}
|
||||
|
||||
func pause() {
|
||||
|
||||
}
|
||||
|
||||
func connect(to consumer: TextureConsumer) {
|
||||
self.output = consumer
|
||||
|
||||
if let texture = self.texture {
|
||||
self.output?.consumeTexture(texture, rotation: .rotate0Degrees)
|
||||
self.output?.consumeTexture(texture)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,3 +71,17 @@ func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTe
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTextureImage(device: MTLDevice, texture: MTLTexture) -> UIImage? {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CIContext(mtlDevice: device, options: [:])
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else {
|
||||
return nil
|
||||
}
|
||||
let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,15 @@ public final class MediaEditor {
|
||||
public var histogram: Signal<Data, NoError> {
|
||||
return self.histogramPromise.get()
|
||||
}
|
||||
|
||||
public var isHistogramEnabled: Bool {
|
||||
get {
|
||||
return self.histogramCalculationPass.isEnabled
|
||||
}
|
||||
set {
|
||||
self.histogramCalculationPass.isEnabled = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var textureCache: CVMetalTextureCache!
|
||||
|
||||
public var hasPortraitMask: Bool {
|
||||
@@ -90,7 +98,7 @@ public final class MediaEditor {
|
||||
public var resultImage: UIImage? {
|
||||
return self.renderer.finalRenderedImage()
|
||||
}
|
||||
|
||||
|
||||
private let playerPromise = Promise<AVPlayer?>()
|
||||
private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) {
|
||||
didSet {
|
||||
@@ -250,6 +258,7 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
|
||||
private var volumeFade: SwiftSignalKit.Timer?
|
||||
private func setupSource() {
|
||||
guard let renderTarget = self.previewView else {
|
||||
return
|
||||
@@ -367,14 +376,19 @@ public final class MediaEditor {
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in
|
||||
if let self {
|
||||
let (source, image, player, topColor, bottomColor) = sourceAndColors
|
||||
self.renderer.onNextRender = { [weak self] in
|
||||
self?.previewView?.removeTransitionImage()
|
||||
}
|
||||
self.renderer.textureSource = source
|
||||
self.player = player
|
||||
self.playerPromise.set(.single(player))
|
||||
self.gradientColorsValue = (topColor, bottomColor)
|
||||
self.setGradientColors([topColor, bottomColor])
|
||||
|
||||
self.maybeGeneratePersonSegmentation(image)
|
||||
|
||||
if player == nil {
|
||||
self.maybeGeneratePersonSegmentation(image)
|
||||
}
|
||||
|
||||
if let player {
|
||||
self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in
|
||||
guard let self, let duration = player.currentItem?.duration.seconds else {
|
||||
@@ -394,6 +408,7 @@ public final class MediaEditor {
|
||||
}
|
||||
})
|
||||
self.player?.play()
|
||||
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -431,16 +446,49 @@ public final class MediaEditor {
|
||||
self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted)
|
||||
}
|
||||
|
||||
public func seek(_ position: Double, andPlay: Bool) {
|
||||
if !andPlay {
|
||||
private var targetTimePosition: (CMTime, Bool)?
|
||||
private var updatingTimePosition = false
|
||||
public func seek(_ position: Double, andPlay play: Bool) {
|
||||
if !play {
|
||||
self.player?.pause()
|
||||
}
|
||||
self.player?.seek(to: CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in })
|
||||
if andPlay {
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
|
||||
if self.targetTimePosition?.0 != targetPosition {
|
||||
self.targetTimePosition = (targetPosition, play)
|
||||
if !self.updatingTimePosition {
|
||||
self.updateVideoTimePosition()
|
||||
}
|
||||
}
|
||||
if play {
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
public func play() {
|
||||
self.player?.play()
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
self.player?.pause()
|
||||
}
|
||||
|
||||
private func updateVideoTimePosition() {
|
||||
guard let (targetPosition, _) = self.targetTimePosition else {
|
||||
return
|
||||
}
|
||||
self.updatingTimePosition = true
|
||||
self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in
|
||||
if let self {
|
||||
if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition {
|
||||
self.updatingTimePosition = false
|
||||
self.targetTimePosition = nil
|
||||
} else {
|
||||
self.updateVideoTimePosition()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func setVideoTrimStart(_ trimStart: Double) {
|
||||
let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
@@ -452,7 +500,7 @@ public final class MediaEditor {
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
self.values = self.values.withUpdatedVideoTrimRange(trimRange)
|
||||
}
|
||||
|
||||
|
||||
public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) {
|
||||
self.values = self.values.withUpdatedDrawingAndEntities(drawing: image, entities: entities)
|
||||
}
|
||||
@@ -583,15 +631,23 @@ final class MediaEditorRenderChain {
|
||||
}
|
||||
case .shadowsTint:
|
||||
if let value = value as? TintValue {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity
|
||||
if value.color != .clear {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity
|
||||
} else {
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0
|
||||
}
|
||||
}
|
||||
case .highlightsTint:
|
||||
if let value = value as? TintValue {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity
|
||||
if value.color != .clear {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity
|
||||
} else {
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0
|
||||
}
|
||||
}
|
||||
case .blur:
|
||||
if let value = value as? BlurValue {
|
||||
@@ -626,3 +682,11 @@ final class MediaEditorRenderChain {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func debugSaveImage(_ image: UIImage, name: String) {
|
||||
let path = NSTemporaryDirectory() + "debug_\(name)_\(Int64.random(in: .min ... .max)).png"
|
||||
print(path)
|
||||
if let data = image.pngData() {
|
||||
try? data.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,26 @@ import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
|
||||
func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
let gradientColors = colors.map { $0.cgColor } as CFArray
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
final class MediaEditorComposer {
|
||||
let device: MTLDevice?
|
||||
private let colorSpace: CGColorSpace
|
||||
|
||||
private let values: MediaEditorValues
|
||||
private let dimensions: CGSize
|
||||
@@ -34,27 +52,28 @@ final class MediaEditorComposer {
|
||||
self.dimensions = dimensions
|
||||
self.outputDimensions = outputDimensions
|
||||
|
||||
self.renderer.addRenderChain(self.renderChain)
|
||||
self.renderer.addRenderPass(ComposerRenderPass())
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
self.gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
self.renderer.addRenderChain(self.renderChain)
|
||||
|
||||
if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) {
|
||||
self.gradientImage = CIImage(image: image, options: [.colorSpace: self.colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
self.gradientImage = CIImage(color: .black)
|
||||
}
|
||||
|
||||
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) {
|
||||
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) {
|
||||
self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
self.drawingImage = nil
|
||||
}
|
||||
|
||||
self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0) }
|
||||
self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) }
|
||||
|
||||
self.device = MTLCreateSystemDefaultDevice()
|
||||
if let device = self.device {
|
||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()])
|
||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : self.colorSpace])
|
||||
|
||||
CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &self.textureCache)
|
||||
} else {
|
||||
@@ -65,46 +84,35 @@ final class MediaEditorComposer {
|
||||
self.renderChain.update(values: self.values)
|
||||
}
|
||||
|
||||
func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, completion: @escaping (CVPixelBuffer?) -> Void) {
|
||||
guard let textureCache = self.textureCache, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else {
|
||||
func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, pool: CVPixelBufferPool?, textureRotation: TextureRotation, completion: @escaping (CVPixelBuffer?) -> Void) {
|
||||
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let pool = pool else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
|
||||
let width = CVPixelBufferGetWidth(imageBuffer)
|
||||
let height = CVPixelBufferGetHeight(imageBuffer)
|
||||
let format: MTLPixelFormat = .bgra8Unorm
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, imageBuffer, nil, format, width, height, 0, &textureRef)
|
||||
var texture: MTLTexture?
|
||||
if status == kCVReturnSuccess {
|
||||
texture = CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
if let texture {
|
||||
self.renderer.consumeTexture(texture, rotation: .rotate90Degrees)
|
||||
self.renderer.renderFrame()
|
||||
self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation)
|
||||
self.renderer.renderFrame()
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
||||
|
||||
if let pixelBuffer {
|
||||
processImage(inputImage: ciImage, time: time, completion: { compositedImage in
|
||||
if var compositedImage {
|
||||
let scale = self.outputDimensions.width / self.dimensions.width
|
||||
compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
|
||||
|
||||
if let pixelBuffer {
|
||||
processImage(inputImage: ciImage, time: time, completion: { compositedImage in
|
||||
if var compositedImage {
|
||||
let scale = self.outputDimensions.width / self.dimensions.width
|
||||
compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
completion(nil)
|
||||
@@ -116,13 +124,12 @@ final class MediaEditorComposer {
|
||||
completion(nil, time)
|
||||
return
|
||||
}
|
||||
if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage {
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
if let texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) {
|
||||
self.renderer.consumeTexture(texture, rotation: .rotate0Degrees)
|
||||
if self.filteredImage == nil, let device = self.device {
|
||||
if let texture = loadTexture(image: inputImage, device: device) {
|
||||
self.renderer.consumeTexture(texture)
|
||||
self.renderer.renderFrame()
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) {
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
self.filteredImage = ciImage
|
||||
}
|
||||
@@ -137,7 +144,7 @@ final class MediaEditorComposer {
|
||||
makeEditorImageFrameComposition(inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in
|
||||
if var compositedImage {
|
||||
let scale = self.outputDimensions.width / self.dimensions.width
|
||||
compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer, time)
|
||||
@@ -157,21 +164,21 @@ final class MediaEditorComposer {
|
||||
}
|
||||
|
||||
public func makeEditorImageComposition(account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) {
|
||||
let inputImage = CIImage(image: inputImage)!
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])!
|
||||
let gradientImage: CIImage
|
||||
var drawingImage: CIImage?
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) {
|
||||
gradientImage = CIImage(image: image, options: [.colorSpace: colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
gradientImage = CIImage(color: .black)
|
||||
}
|
||||
|
||||
if let drawing = values.drawing, let image = CIImage(image: drawing) {
|
||||
if let drawing = values.drawing, let image = CIImage(image: drawing, options: [.colorSpace: colorSpace]) {
|
||||
drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
}
|
||||
|
||||
let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0) }
|
||||
let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) }
|
||||
makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in
|
||||
if let ciImage {
|
||||
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
||||
@@ -190,7 +197,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
resultImage = gradientImage.composited(over: resultImage)
|
||||
|
||||
var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
|
||||
var initialScale: CGFloat
|
||||
if mediaImage.extent.height > mediaImage.extent.width {
|
||||
@@ -206,7 +213,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
resultImage = mediaImage.composited(over: resultImage)
|
||||
|
||||
if let drawingImage {
|
||||
resultImage = drawingImage.composited(over: resultImage)
|
||||
resultImage = drawingImage.samplingLinear().composited(over: resultImage)
|
||||
}
|
||||
|
||||
let frameRate: Float = 30.0
|
||||
@@ -235,7 +242,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
}
|
||||
let index = i
|
||||
entity.image(for: time, frameRate: frameRate, completion: { image in
|
||||
if var image = image {
|
||||
if var image = image?.samplingLinear() {
|
||||
let resetTransform = CGAffineTransform(translationX: -image.extent.width / 2.0, y: -image.extent.height / 2.0)
|
||||
image = image.transformed(by: resetTransform)
|
||||
|
||||
@@ -266,7 +273,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
maybeFinalize()
|
||||
}
|
||||
|
||||
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity) -> MediaEditorComposerEntity? {
|
||||
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
@@ -275,8 +282,8 @@ private func composerEntityForDrawingEntity(account: Account, entity: DrawingEnt
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
}
|
||||
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) {
|
||||
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]) {
|
||||
if let entity = entity as? DrawingBubbleEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
@@ -331,6 +338,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
let colorSpace: CGColorSpace
|
||||
|
||||
var isAnimated: Bool
|
||||
var source: AnimatedStickerNodeSource?
|
||||
@@ -349,13 +357,14 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
let imagePromise = Promise<UIImage>()
|
||||
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) {
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
|
||||
self.content = content
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
switch content {
|
||||
case let .file(file):
|
||||
@@ -373,7 +382,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let queue = strongSelf.queue
|
||||
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
|
||||
//return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
|
||||
})
|
||||
frameSource.syncWith { frameSource in
|
||||
strongSelf.frameCount = frameSource.frameCount
|
||||
@@ -391,13 +399,13 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
} else {
|
||||
self.isAnimated = false
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||
if let strongSelf = self {
|
||||
if let self {
|
||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||
let image = context?.generateImage()
|
||||
if let image = image {
|
||||
strongSelf.imagePromise.set(.single(image))
|
||||
let image = context?.generateImage(colorSpace: self.colorSpace)
|
||||
if let image {
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -488,7 +496,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
|
||||
if let imagePixelBuffer {
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor)
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor)
|
||||
strongSelf.image = image
|
||||
}
|
||||
completion(strongSelf.image)
|
||||
@@ -508,9 +516,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let _ = (self.imagePromise.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] image in
|
||||
if let strongSelf = self {
|
||||
strongSelf.image = CIImage(image: image)
|
||||
completion(strongSelf.image)
|
||||
if let self {
|
||||
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
|
||||
completion(self.image)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -528,7 +536,7 @@ protocol MediaEditorComposerEntity {
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
|
||||
}
|
||||
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? {
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? {
|
||||
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
|
||||
//assert(bytesPerRow == calculatedBytesPerRow)
|
||||
|
||||
@@ -557,56 +565,5 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type:
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
return CIImage(cvPixelBuffer: pixelBuffer)
|
||||
}
|
||||
|
||||
final class ComposerRenderPass: DefaultRenderPass {
|
||||
fileprivate var cachedTexture: MTLTexture?
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = input.pixelFormat
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "composerTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture!
|
||||
}
|
||||
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace])
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
|
||||
func scheduleFrame() {
|
||||
Queue.mainQueue().async {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.draw()
|
||||
}
|
||||
}
|
||||
@@ -67,4 +67,33 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
self.renderer?.renderFrame()
|
||||
}
|
||||
|
||||
private var transitionView: UIImageView?
|
||||
public func setTransitionImage(_ image: UIImage) {
|
||||
self.transitionView?.removeFromSuperview()
|
||||
|
||||
let transitionView = UIImageView(image: image)
|
||||
transitionView.frame = self.bounds
|
||||
self.addSubview(transitionView)
|
||||
|
||||
self.transitionView = transitionView
|
||||
}
|
||||
|
||||
public func removeTransitionImage() {
|
||||
if let transitionView = self.transitionView {
|
||||
// transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak transitionView] _ in
|
||||
//
|
||||
// })
|
||||
transitionView.removeFromSuperview()
|
||||
self.transitionView = nil
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if let transitionView = self.transitionView {
|
||||
transitionView.frame = self.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import Photos
|
||||
import SwiftSignalKit
|
||||
|
||||
protocol TextureConsumer: AnyObject {
|
||||
func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation)
|
||||
func consumeTexture(_ texture: MTLTexture)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation)
|
||||
}
|
||||
|
||||
final class RenderingContext {
|
||||
@@ -24,7 +25,7 @@ final class RenderingContext {
|
||||
|
||||
protocol RenderPass: AnyObject {
|
||||
func setup(device: MTLDevice, library: MTLLibrary)
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture?
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture?
|
||||
}
|
||||
|
||||
protocol TextureSource {
|
||||
@@ -51,7 +52,9 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
|
||||
var semaphore = DispatchSemaphore(value: 3)
|
||||
private var renderPasses: [RenderPass] = []
|
||||
private var outputRenderPass = OutputRenderPass()
|
||||
|
||||
private let videoInputPass = VideoInputPass()
|
||||
private let outputRenderPass = OutputRenderPass()
|
||||
private weak var renderTarget: RenderTarget? {
|
||||
didSet {
|
||||
self.outputRenderPass.renderTarget = self.renderTarget
|
||||
@@ -59,10 +62,14 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
|
||||
private var device: MTLDevice?
|
||||
private var commandQueue: MTLCommandQueue?
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentRotation: TextureRotation = .rotate0Degrees
|
||||
private var library: MTLLibrary?
|
||||
private var commandQueue: MTLCommandQueue?
|
||||
private var textureCache: CVMetalTextureCache?
|
||||
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentPixelBuffer: (CVPixelBuffer, TextureRotation)?
|
||||
|
||||
public var onNextRender: (() -> Void)?
|
||||
|
||||
var finalTexture: MTLTexture?
|
||||
|
||||
@@ -94,6 +101,8 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||
|
||||
let mainBundle = Bundle(for: MediaEditorRenderer.self)
|
||||
guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else {
|
||||
return
|
||||
@@ -102,15 +111,16 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return
|
||||
}
|
||||
self.library = defaultLibrary
|
||||
self.library = library
|
||||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) }
|
||||
self.outputRenderPass.setup(device: device, library: defaultLibrary)
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
self.outputRenderPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func setupForComposer(composer: MediaEditorComposer) {
|
||||
@@ -118,6 +128,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
self.device = device
|
||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||
|
||||
let mainBundle = Bundle(for: MediaEditorRenderer.self)
|
||||
guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else {
|
||||
@@ -127,17 +138,17 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return
|
||||
}
|
||||
self.library = defaultLibrary
|
||||
self.library = library
|
||||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) }
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
}
|
||||
|
||||
private var currentCommandBuffer: MTLCommandBuffer?
|
||||
func renderFrame() {
|
||||
let device: MTLDevice?
|
||||
if let renderTarget = self.renderTarget {
|
||||
@@ -149,7 +160,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
guard let device = device,
|
||||
let commandQueue = self.commandQueue,
|
||||
var texture = self.currentTexture else {
|
||||
let textureCache = self.textureCache else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -157,22 +168,36 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
var rotation: TextureRotation = self.currentRotation
|
||||
var texture: MTLTexture
|
||||
if let currentTexture = self.currentTexture {
|
||||
texture = currentTexture
|
||||
} else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) {
|
||||
texture = videoTexture
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
for renderPass in self.renderPasses {
|
||||
if let nextTexture = renderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) {
|
||||
if nextTexture !== texture {
|
||||
rotation = .rotate0Degrees
|
||||
}
|
||||
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
|
||||
texture = nextTexture
|
||||
}
|
||||
}
|
||||
if self.renderTarget != nil {
|
||||
let _ = self.outputRenderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let _ = self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
self.finalTexture = texture
|
||||
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
self?.semaphore.signal()
|
||||
if let self {
|
||||
self.semaphore.signal()
|
||||
|
||||
if let onNextRender = self.onNextRender {
|
||||
self.onNextRender = nil
|
||||
Queue.mainQueue().async {
|
||||
onNextRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = self.renderTarget {
|
||||
@@ -184,18 +209,17 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
func commit() {
|
||||
if let commandBuffer = self.currentCommandBuffer {
|
||||
commandBuffer.commit()
|
||||
self.currentCommandBuffer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) {
|
||||
func consumeTexture(_ texture: MTLTexture) {
|
||||
self.semaphore.wait()
|
||||
|
||||
self.currentTexture = texture
|
||||
self.currentRotation = rotation
|
||||
self.renderTarget?.scheduleFrame()
|
||||
}
|
||||
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) {
|
||||
self.semaphore.wait()
|
||||
|
||||
self.currentPixelBuffer = (pixelBuffer, rotation)
|
||||
self.renderTarget?.scheduleFrame()
|
||||
}
|
||||
|
||||
@@ -209,27 +233,10 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
|
||||
func finalRenderedImage() -> UIImage? {
|
||||
if let finalTexture = self.finalTexture {
|
||||
return getTextureImage(finalTexture)
|
||||
if let finalTexture = self.finalTexture, let device = self.renderTarget?.mtlDevice {
|
||||
return getTextureImage(device: device, texture: finalTexture)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func getTextureImage(_ texture: MTLTexture) -> UIImage? {
|
||||
guard let device = self.renderTarget?.mtlDevice else {
|
||||
return nil
|
||||
}
|
||||
let options = [CIImageOption.colorSpace: CGColorSpaceCreateDeviceRGB()]
|
||||
let context = CIContext(mtlDevice: device)
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: options) else {
|
||||
return nil
|
||||
}
|
||||
let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
|
||||
extension AVPlayer {
|
||||
func fadeVolume(from: Float, to: Float, duration: Float, completion: (() -> Void)? = nil) -> SwiftSignalKit.Timer? {
|
||||
self.volume = from
|
||||
guard from != to else { return nil }
|
||||
|
||||
let interval: Float = 0.1
|
||||
let range = to - from
|
||||
let step = (range * interval) / duration
|
||||
|
||||
func reachedTarget() -> Bool {
|
||||
guard self.volume >= 0, self.volume <= 1 else {
|
||||
self.volume = to
|
||||
return true
|
||||
}
|
||||
|
||||
if to > from {
|
||||
return self.volume >= to
|
||||
}
|
||||
return self.volume <= to
|
||||
}
|
||||
|
||||
var invalidateImpl: (() -> Void)?
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double(interval), repeat: true, completion: { [weak self] in
|
||||
if let self, !reachedTarget() {
|
||||
self.volume += step
|
||||
} else {
|
||||
invalidateImpl?()
|
||||
completion?()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
invalidateImpl = { [weak timer] in
|
||||
timer?.invalidate()
|
||||
}
|
||||
timer.start()
|
||||
return timer
|
||||
}
|
||||
}
|
||||
@@ -247,10 +247,7 @@ public final class MediaEditorVideoExport {
|
||||
private let subject: Subject
|
||||
private let configuration: Configuration
|
||||
private let outputPath: String
|
||||
|
||||
private var previousSampleTime: CMTime = .zero
|
||||
private var processedPixelBuffer: CVPixelBuffer?
|
||||
|
||||
|
||||
private var reader: AVAssetReader?
|
||||
|
||||
private var videoOutput: AVAssetReaderOutput?
|
||||
@@ -260,6 +257,7 @@ public final class MediaEditorVideoExport {
|
||||
private var writer: MediaEditorVideoExportWriter?
|
||||
private var composer: MediaEditorComposer?
|
||||
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
private let duration = ValuePromise<CMTime>()
|
||||
|
||||
private let pauseDispatchGroup = DispatchGroup()
|
||||
@@ -320,16 +318,23 @@ public final class MediaEditorVideoExport {
|
||||
return
|
||||
}
|
||||
|
||||
self.textureRotation = textureRotatonForAVAsset(asset)
|
||||
|
||||
writer.setup(configuration: self.configuration, outputPath: self.outputPath)
|
||||
|
||||
let videoTracks = asset.tracks(withMediaType: .video)
|
||||
if (videoTracks.count > 0) {
|
||||
let outputSettings: [String : Any]
|
||||
var sourceFrameRate: Float = 0.0
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
|
||||
AVVideoColorPropertiesKey: [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
]
|
||||
if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing {
|
||||
outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange]]
|
||||
} else {
|
||||
outputSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
|
||||
self.setupComposer()
|
||||
}
|
||||
let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings)
|
||||
@@ -516,7 +521,7 @@ public final class MediaEditorVideoExport {
|
||||
if let buffer = output.copyNextSampleBuffer() {
|
||||
if let composer = self.composer {
|
||||
let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, completion: { pixelBuffer in
|
||||
composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in
|
||||
if let pixelBuffer {
|
||||
if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) {
|
||||
writer.markVideoAsFinished()
|
||||
|
||||
@@ -102,7 +102,7 @@ class DefaultRenderPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation) {
|
||||
func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation = .rotate0Degrees) {
|
||||
if self.verticesBuffer == nil || rotation != self.textureRotation {
|
||||
self.textureRotation = rotation
|
||||
let vertices = verticesDataForRotation(rotation)
|
||||
@@ -113,8 +113,8 @@ class DefaultRenderPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ class DefaultRenderPass: RenderPass {
|
||||
final class OutputRenderPass: DefaultRenderPass {
|
||||
weak var renderTarget: RenderTarget?
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else {
|
||||
return nil
|
||||
}
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let drawableSize = renderTarget.drawableSize
|
||||
|
||||
@@ -147,23 +147,8 @@ final class OutputRenderPass: DefaultRenderPass {
|
||||
width: Double(drawableSize.width), height: Double(drawableSize.height),
|
||||
znear: -1.0, zfar: 1.0))
|
||||
|
||||
do {
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
var scaleFactor = drawableSize.width / CGFloat(input.width)
|
||||
let textureFitHeight = CGFloat(input.height) * scaleFactor
|
||||
if textureFitHeight > drawableSize.height {
|
||||
scaleFactor = drawableSize.height / CGFloat(input.height)
|
||||
let textureFitWidth = CGFloat(input.width) * scaleFactor
|
||||
let texCoordsScaleX = textureFitWidth / drawableSize.width
|
||||
texCoordScales.x = Float(texCoordsScaleX)
|
||||
} else {
|
||||
let texCoordsScaleY = textureFitHeight / drawableSize.height
|
||||
texCoordScales.y = Float(texCoordsScaleY)
|
||||
}
|
||||
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
}
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ final class SharpenRenderPass: RenderPass {
|
||||
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@ import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
|
||||
func textureRotatonForAVAsset(_ asset: AVAsset) -> TextureRotation {
|
||||
for track in asset.tracks {
|
||||
if track.mediaType == .video {
|
||||
let t = track.preferredTransform
|
||||
if t.a == -1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else if t.a == 1.0 && t.d == 1.0 {
|
||||
return .rotate0Degrees
|
||||
} else if t.b == -1.0 && t.c == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == -1.0 && t.d == 1.0 {
|
||||
return .rotate270Degrees
|
||||
} else if t.a == 1.0 && t.d == -1.0 {
|
||||
return .rotate180Degrees
|
||||
} else {
|
||||
return .rotate90Degrees
|
||||
}
|
||||
}
|
||||
}
|
||||
return .rotate0Degrees
|
||||
}
|
||||
|
||||
final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullDelegate {
|
||||
private let player: AVPlayer
|
||||
private var playerItem: AVPlayerItem?
|
||||
@@ -13,22 +35,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
private let device: MTLDevice?
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
|
||||
private var forceUpdate: Bool = false
|
||||
|
||||
weak var output: TextureConsumer?
|
||||
var textureCache: CVMetalTextureCache!
|
||||
var queue: DispatchQueue!
|
||||
var started: Bool = false
|
||||
|
||||
init(player: AVPlayer, renderTarget: RenderTarget) {
|
||||
self.player = player
|
||||
|
||||
if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess {
|
||||
print("error")
|
||||
}
|
||||
|
||||
self.device = renderTarget.mtlDevice!
|
||||
|
||||
self.queue = DispatchQueue(
|
||||
label: "VideoTextureSource Queue",
|
||||
qos: .userInteractive,
|
||||
@@ -47,7 +66,8 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
self.playerItemObservation?.invalidate()
|
||||
self.playerItemStatusObservation?.invalidate()
|
||||
}
|
||||
|
||||
private func updatePlayerItem(_ playerItem: AVPlayerItem?) {
|
||||
@@ -63,7 +83,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
self.playerItemStatusObservation = nil
|
||||
|
||||
self.playerItem = playerItem
|
||||
self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial,.new], changeHandler: { [weak self] item, change in
|
||||
self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial, .new], changeHandler: { [weak self] item, change in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@@ -82,40 +102,29 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
for track in playerItem.asset.tracks {
|
||||
if track.mediaType == .video {
|
||||
hasVideoTrack = true
|
||||
|
||||
let t = track.preferredTransform
|
||||
if t.a == -1.0 && t.d == -1.0 {
|
||||
self.textureRotation = .rotate180Degrees
|
||||
} else if t.a == 1.0 && t.d == 1.0 {
|
||||
self.textureRotation = .rotate0Degrees
|
||||
} else if t.b == -1.0 && t.c == 1.0 {
|
||||
self.textureRotation = .rotate270Degrees
|
||||
} else if t.a == -1.0 && t.d == 1.0 {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
self.textureRotation = .rotate270Degrees
|
||||
} else if t.a == 1.0 && t.d == -1.0 {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
self.textureRotation = .rotate180Degrees
|
||||
} else {
|
||||
// if (t.c == 1) {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
// }
|
||||
self.textureRotation = .rotate90Degrees
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
self.textureRotation = textureRotatonForAVAsset(playerItem.asset)
|
||||
if !hasVideoTrack {
|
||||
assertionFailure("No video track found.")
|
||||
return
|
||||
}
|
||||
|
||||
let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as NSString as String: kCVPixelFormatType_32BGRA])
|
||||
let colorProperties: [String: Any] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true,
|
||||
AVVideoColorPropertiesKey: colorProperties
|
||||
]
|
||||
|
||||
let output = AVPlayerItemVideoOutput(outputSettings: outputSettings)
|
||||
output.suppressesPlayerRendering = true
|
||||
output.setDelegate(self, queue: self.queue)
|
||||
playerItem.add(output)
|
||||
self.playerItemOutput = output
|
||||
@@ -174,12 +183,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
var presentationTime: CMTime = .zero
|
||||
if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) {
|
||||
if let texture = self.pixelBufferToMTLTexture(pixelBuffer: pixelBuffer) {
|
||||
self.output?.consumeTexture(texture, rotation: self.textureRotation)
|
||||
}
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setNeedsUpdate() {
|
||||
self.displayLink?.isPaused = false
|
||||
self.forceUpdate = true
|
||||
@@ -196,19 +203,148 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
self.output = consumer
|
||||
}
|
||||
|
||||
private func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer) -> MTLTexture? {
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
let format: MTLPixelFormat = .bgra8Unorm
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef)
|
||||
if status == kCVReturnSuccess {
|
||||
return CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) {
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoInputPass: DefaultRenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
private let scalePass = VideoInputScalePass()
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
return "bt709ToRGBFragmentShader"
|
||||
}
|
||||
|
||||
override func setup(device: MTLDevice, library: MTLLibrary) {
|
||||
super.setup(device: device, library: library)
|
||||
self.scalePass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func processPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? {
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef)
|
||||
if status == kCVReturnSuccess, let textureRef {
|
||||
return CVMetalTextureGetTexture(textureRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
guard let inputYTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0),
|
||||
let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else {
|
||||
return nil
|
||||
}
|
||||
return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) {
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees:
|
||||
return (height, width)
|
||||
default:
|
||||
return (width, height)
|
||||
}
|
||||
}
|
||||
|
||||
let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation)
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = outputWidth
|
||||
textureDescriptor.height = outputHeight
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
if let texture = device.makeTexture(descriptor: textureDescriptor) {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(outputWidth), height: Double(outputHeight),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(yTexture, index: 0)
|
||||
renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
var outputTexture = self.cachedTexture
|
||||
if let texture = outputTexture {
|
||||
outputTexture = self.scalePass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
return outputTexture
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoInputScalePass: DefaultRenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard max(input.width, input.height) > 1920 else {
|
||||
return input
|
||||
}
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let scaledSize = CGSize(width: input.width, height: input.height).fitted(CGSize(width: 1920.0, height: 1920.0))
|
||||
let width = Int(scaledSize.width)
|
||||
let height = Int(scaledSize.height)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = input.pixelFormat
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "scaledVideoTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture!
|
||||
}
|
||||
}
|
||||
|
||||
+586
-163
File diff suppressed because it is too large
Load Diff
@@ -520,6 +520,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
var needsHistogram = false
|
||||
let screenSize: CGSize
|
||||
let optionsSize: CGSize
|
||||
let optionsTransition: Transition = sectionChanged ? .immediate : transition
|
||||
@@ -711,6 +712,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
case .curves:
|
||||
needsHistogram = true
|
||||
let internalState: CurvesInternalState
|
||||
if let current = self.curvesState {
|
||||
internalState = current
|
||||
@@ -755,6 +757,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
}
|
||||
component.mediaEditor.isHistogramEnabled = needsHistogram
|
||||
|
||||
let optionsFrame = CGRect(origin: .zero, size: optionsSize)
|
||||
if let optionsView = self.toolOptions.view {
|
||||
|
||||
+31
-16
@@ -23,10 +23,10 @@ final class VideoScrubberComponent: Component {
|
||||
let startPosition: Double
|
||||
let endPosition: Double
|
||||
let position: Double
|
||||
let maxDuration: Double
|
||||
let frames: [UIImage]
|
||||
let framesUpdateTimestamp: Double
|
||||
let startPositionUpdated: (Double, Bool) -> Void
|
||||
let endPositionUpdated: (Double, Bool) -> Void
|
||||
let trimUpdated: (Double, Double, Bool, Bool) -> Void
|
||||
let positionUpdated: (Double, Bool) -> Void
|
||||
|
||||
init(
|
||||
@@ -35,10 +35,10 @@ final class VideoScrubberComponent: Component {
|
||||
startPosition: Double,
|
||||
endPosition: Double,
|
||||
position: Double,
|
||||
maxDuration: Double,
|
||||
frames: [UIImage],
|
||||
framesUpdateTimestamp: Double,
|
||||
startPositionUpdated: @escaping (Double, Bool) -> Void,
|
||||
endPositionUpdated: @escaping (Double, Bool) -> Void,
|
||||
trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void,
|
||||
positionUpdated: @escaping (Double, Bool) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
@@ -46,10 +46,10 @@ final class VideoScrubberComponent: Component {
|
||||
self.startPosition = startPosition
|
||||
self.endPosition = endPosition
|
||||
self.position = position
|
||||
self.maxDuration = maxDuration
|
||||
self.frames = frames
|
||||
self.framesUpdateTimestamp = framesUpdateTimestamp
|
||||
self.startPositionUpdated = startPositionUpdated
|
||||
self.endPositionUpdated = endPositionUpdated
|
||||
self.trimUpdated = trimUpdated
|
||||
self.positionUpdated = positionUpdated
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ final class VideoScrubberComponent: Component {
|
||||
if lhs.position != rhs.position {
|
||||
return false
|
||||
}
|
||||
if lhs.maxDuration != rhs.maxDuration {
|
||||
return false
|
||||
}
|
||||
if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp {
|
||||
return false
|
||||
}
|
||||
@@ -165,22 +168,28 @@ final class VideoScrubberComponent: Component {
|
||||
let end = self.frame.width - handleWidth
|
||||
let length = end - start
|
||||
let fraction = (location.x - start) / length
|
||||
var value = max(0.0, component.duration * fraction)
|
||||
if value > component.endPosition - minumumDuration {
|
||||
value = max(0.0, component.endPosition - minumumDuration)
|
||||
|
||||
var startValue = max(0.0, component.duration * fraction)
|
||||
if startValue > component.endPosition - minumumDuration {
|
||||
startValue = max(0.0, component.endPosition - minumumDuration)
|
||||
}
|
||||
var endValue = component.endPosition
|
||||
if endValue - startValue > component.maxDuration {
|
||||
let delta = (endValue - startValue) - component.maxDuration
|
||||
endValue -= delta
|
||||
}
|
||||
|
||||
var transition: Transition = .immediate
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.isPanningHandle = true
|
||||
component.startPositionUpdated(value, false)
|
||||
component.trimUpdated(startValue, endValue, false, false)
|
||||
if case .began = gestureRecognizer.state {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.isPanningHandle = false
|
||||
component.startPositionUpdated(value, true)
|
||||
component.trimUpdated(startValue, endValue, false, true)
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
default:
|
||||
break
|
||||
@@ -197,22 +206,28 @@ final class VideoScrubberComponent: Component {
|
||||
let end = self.frame.width - handleWidth
|
||||
let length = end - start
|
||||
let fraction = (location.x - start) / length
|
||||
var value = min(component.duration, component.duration * fraction)
|
||||
if value < component.startPosition + minumumDuration {
|
||||
value = min(component.duration, component.startPosition + minumumDuration)
|
||||
|
||||
var endValue = min(component.duration, component.duration * fraction)
|
||||
if endValue < component.startPosition + minumumDuration {
|
||||
endValue = min(component.duration, component.startPosition + minumumDuration)
|
||||
}
|
||||
var startValue = component.startPosition
|
||||
if endValue - startValue > component.maxDuration {
|
||||
let delta = (endValue - startValue) - component.maxDuration
|
||||
startValue += delta
|
||||
}
|
||||
|
||||
var transition: Transition = .immediate
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.isPanningHandle = true
|
||||
component.endPositionUpdated(value, false)
|
||||
component.trimUpdated(startValue, endValue, true, false)
|
||||
if case .began = gestureRecognizer.state {
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.isPanningHandle = false
|
||||
component.endPositionUpdated(value, true)
|
||||
component.trimUpdated(startValue, endValue, true, true)
|
||||
transition = .easeInOut(duration: 0.25)
|
||||
default:
|
||||
break
|
||||
|
||||
+14
@@ -223,6 +223,20 @@ public final class MessageInputActionButtonComponent: Component {
|
||||
if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName {
|
||||
if let iconName = component.mode.iconName {
|
||||
self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white)
|
||||
} else if case .apply = component.mode {
|
||||
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let image = UIImage(bundleImageName: "Media Editor/Apply"), let cgImage = image.cgImage {
|
||||
context.setBlendMode(.clear)
|
||||
context.clip(to: CGRect(origin: CGPoint(x: -4.0 + UIScreenPixel, y: -3.0 - UIScreenPixel), size: CGSize(width: 40.0, height: 40.0)), mask: cgImage)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
})
|
||||
} else if case .none = component.mode {
|
||||
self.sendIconView.image = nil
|
||||
} else {
|
||||
self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
+4
-4
@@ -239,7 +239,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0)
|
||||
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 41.0)
|
||||
|
||||
if let _ = component.attachmentAction {
|
||||
insets.left = 41.0
|
||||
@@ -321,8 +321,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
if self.textFieldExternalState.isEditing {
|
||||
insets.right = 41.0
|
||||
if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil {
|
||||
insets.right = insets.left
|
||||
}
|
||||
|
||||
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
|
||||
@@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if timeoutButtonView.superview == nil {
|
||||
self.addSubview(timeoutButtonView)
|
||||
}
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - timeoutButtonSize.height) * 0.5)), size: timeoutButtonSize)
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
|
||||
"//submodules/TelegramUI/Components/TokenListTextField",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/PeerPresenceStatusManager",
|
||||
"//submodules/LocalizedPeerData"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+31
-3
@@ -10,6 +10,7 @@ import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import BundleIconComponent
|
||||
|
||||
final class CategoryListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
@@ -27,6 +28,7 @@ final class CategoryListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: () -> Void
|
||||
let secondaryAction: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@@ -38,7 +40,8 @@ final class CategoryListItemComponent: Component {
|
||||
subtitle: String?,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping () -> Void
|
||||
action: @escaping () -> Void,
|
||||
secondaryAction: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@@ -50,6 +53,7 @@ final class CategoryListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.secondaryAction = secondaryAction
|
||||
}
|
||||
|
||||
static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool {
|
||||
@@ -88,6 +92,7 @@ final class CategoryListItemComponent: Component {
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let labelArrow = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let iconView: UIImageView
|
||||
|
||||
@@ -120,7 +125,11 @@ final class CategoryListItemComponent: Component {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
if case .editing(true, _) = component.selectionState {
|
||||
component.secondaryAction()
|
||||
} else {
|
||||
component.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
@@ -223,7 +232,12 @@ final class CategoryListItemComponent: Component {
|
||||
|
||||
transition.setFrame(view: self.iconView, frame: avatarFrame)
|
||||
|
||||
let labelData: (String, Bool) = ("", false)
|
||||
let labelData: (String, Bool, Bool)
|
||||
if let subtitle = component.subtitle {
|
||||
labelData = (subtitle, true, true)
|
||||
} else {
|
||||
labelData = ("", false, false)
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
@@ -234,6 +248,13 @@ final class CategoryListItemComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let labelArrowSize = self.labelArrow.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(name: "Contact List/SubtitleArrow", tintColor: component.theme.list.itemAccentColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
@@ -284,6 +305,13 @@ final class CategoryListItemComponent: Component {
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||
}
|
||||
if let labelArrowView = self.labelArrow.view, !labelData.0.isEmpty {
|
||||
if labelArrowView.superview == nil {
|
||||
labelArrowView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelArrowView)
|
||||
}
|
||||
transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0)), size: labelArrowSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
|
||||
+351
-231
@@ -17,6 +17,7 @@ import PlainButtonComponent
|
||||
import AnimatedCounterComponent
|
||||
import TokenListTextField
|
||||
import AvatarNode
|
||||
import LocalizedPeerData
|
||||
|
||||
final class ShareWithPeersScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@@ -26,19 +27,25 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
let initialPrivacy: EngineStoryPrivacy
|
||||
let categoryItems: [CategoryItem]
|
||||
let completion: (EngineStoryPrivacy) -> Void
|
||||
let editCategory: (EngineStoryPrivacy) -> Void
|
||||
let secondaryAction: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
stateContext: ShareWithPeersScreen.StateContext,
|
||||
initialPrivacy: EngineStoryPrivacy,
|
||||
categoryItems: [CategoryItem],
|
||||
completion: @escaping (EngineStoryPrivacy) -> Void
|
||||
completion: @escaping (EngineStoryPrivacy) -> Void,
|
||||
editCategory: @escaping (EngineStoryPrivacy) -> Void,
|
||||
secondaryAction: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.stateContext = stateContext
|
||||
self.initialPrivacy = initialPrivacy
|
||||
self.categoryItems = categoryItems
|
||||
self.completion = completion
|
||||
self.editCategory = editCategory
|
||||
self.secondaryAction = secondaryAction
|
||||
}
|
||||
|
||||
static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool {
|
||||
@@ -204,6 +211,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
private let navigationBackgroundView: BlurredBackgroundView
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let navigationLeftButton = ComponentView<Empty>()
|
||||
private let navigationRightButton = ComponentView<Empty>()
|
||||
private let navigationSeparatorLayer: SimpleLayer
|
||||
private let navigationTextFieldState = TokenListTextField.ExternalState()
|
||||
private let navigationTextField = ComponentView<Empty>()
|
||||
@@ -440,7 +448,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if section.id == 0 {
|
||||
sectionTitle = "WHO CAN VIEW FOR 24 HOURS"
|
||||
} else {
|
||||
sectionTitle = "CONTACTS"
|
||||
if case .chats = component.stateContext.subject {
|
||||
sectionTitle = "CHATS"
|
||||
} else {
|
||||
sectionTitle = "CONTACTS"
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sectionHeader.update(
|
||||
@@ -521,6 +533,26 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
},
|
||||
secondaryAction: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let base: EngineStoryPrivacy.Base?
|
||||
switch categoryId {
|
||||
case .everyone:
|
||||
base = nil
|
||||
case .contacts:
|
||||
base = .contacts
|
||||
case .closeFriends:
|
||||
base = .closeFriends
|
||||
case .selectedContacts:
|
||||
base = .nobody
|
||||
}
|
||||
if let base {
|
||||
component.editCategory(EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers))
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@@ -700,6 +732,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
var applyState = false
|
||||
self.defaultStateValue = component.stateContext.stateValue
|
||||
self.selectedPeers = Array(component.stateContext.initialPeerIds)
|
||||
|
||||
self.stateDisposable = (component.stateContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stateValue in
|
||||
guard let self else {
|
||||
@@ -738,97 +772,85 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
}
|
||||
|
||||
var tokens: [TokenListTextField.Token] = []
|
||||
for categoryId in self.selectedCategories.sorted(by: { $0.rawValue < $1.rawValue }) {
|
||||
let categoryTitle: String
|
||||
var categoryImage: UIImage?
|
||||
switch categoryId {
|
||||
case .everyone:
|
||||
categoryTitle = "Everyone"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue)
|
||||
case .contacts:
|
||||
categoryTitle = "Contacts"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.9, cornerRadius: 6.0, circleCorners: true, color: .yellow)
|
||||
case .closeFriends:
|
||||
categoryTitle = "Close Friends"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .green)
|
||||
case .selectedContacts:
|
||||
categoryTitle = "Selected Contacts"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .purple)
|
||||
}
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(categoryId),
|
||||
title: categoryTitle,
|
||||
fixedPosition: categoryId.rawValue,
|
||||
content: .category(categoryImage)
|
||||
))
|
||||
}
|
||||
for peerId in self.selectedPeers {
|
||||
guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else {
|
||||
continue
|
||||
}
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(peerId),
|
||||
title: peer.compactDisplayTitle,
|
||||
fixedPosition: nil,
|
||||
content: .peer(peer)
|
||||
))
|
||||
}
|
||||
|
||||
self.navigationTextField.parentState = state
|
||||
let navigationTextFieldSize = self.navigationTextField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TokenListTextField(
|
||||
externalState: self.navigationTextFieldState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
placeholder: "Search Contacts",
|
||||
tokens: tokens,
|
||||
sideInset: sideInset,
|
||||
deleteToken: { [weak self] tokenId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let categoryId = tokenId.base as? CategoryId {
|
||||
self.selectedCategories.remove(categoryId)
|
||||
} else if let peerId = tokenId.base as? EnginePeer.Id {
|
||||
self.selectedPeers.removeAll(where: { $0 == peerId })
|
||||
}
|
||||
if self.selectedCategories.isEmpty {
|
||||
self.selectedCategories.insert(.everyone)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
let navigationTextFieldSize: CGSize
|
||||
if case .stories = component.stateContext.subject {
|
||||
navigationTextFieldSize = .zero
|
||||
} else {
|
||||
var tokens: [TokenListTextField.Token] = []
|
||||
for peerId in self.selectedPeers {
|
||||
guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else {
|
||||
continue
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
if !self.navigationTextFieldState.text.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) {
|
||||
} else {
|
||||
self.searchStateDisposable?.dispose()
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text))
|
||||
var applyState = false
|
||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.searchStateContext = searchStateContext
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true)))
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(peerId),
|
||||
title: peer.compactDisplayTitle,
|
||||
fixedPosition: nil,
|
||||
content: .peer(peer)
|
||||
))
|
||||
}
|
||||
} else if let _ = self.searchStateContext {
|
||||
self.searchStateContext = nil
|
||||
self.searchStateDisposable?.dispose()
|
||||
self.searchStateDisposable = nil
|
||||
|
||||
contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true))
|
||||
let placeholder: String
|
||||
switch component.stateContext.subject {
|
||||
case .chats:
|
||||
placeholder = "Search Chats"
|
||||
default:
|
||||
placeholder = "Search Contacts"
|
||||
}
|
||||
self.navigationTextField.parentState = state
|
||||
navigationTextFieldSize = self.navigationTextField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TokenListTextField(
|
||||
externalState: self.navigationTextFieldState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
placeholder: placeholder,
|
||||
tokens: tokens,
|
||||
sideInset: sideInset,
|
||||
deleteToken: { [weak self] tokenId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let categoryId = tokenId.base as? CategoryId {
|
||||
self.selectedCategories.remove(categoryId)
|
||||
} else if let peerId = tokenId.base as? EnginePeer.Id {
|
||||
self.selectedPeers.removeAll(where: { $0 == peerId })
|
||||
}
|
||||
if self.selectedCategories.isEmpty {
|
||||
self.selectedCategories.insert(.everyone)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
if !self.navigationTextFieldState.text.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) {
|
||||
} else {
|
||||
self.searchStateDisposable?.dispose()
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text))
|
||||
var applyState = false
|
||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.searchStateContext = searchStateContext
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true)))
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
}
|
||||
} else if let _ = self.searchStateContext {
|
||||
self.searchStateContext = nil
|
||||
self.searchStateDisposable?.dispose()
|
||||
self.searchStateDisposable = nil
|
||||
|
||||
contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
let categoryItemSize = self.categoryTemplateItem.update(
|
||||
@@ -843,7 +865,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
subtitle: nil,
|
||||
selectionState: .editing(isSelected: false, isTinted: false),
|
||||
hasNext: true,
|
||||
action: {}
|
||||
action: {},
|
||||
secondaryAction: {}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
@@ -870,54 +893,121 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
var sections: [ItemLayout.Section] = []
|
||||
if let stateValue = self.effectiveStateValue {
|
||||
if self.searchStateContext == nil {
|
||||
if case .stories = component.stateContext.subject {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 0,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0),
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: categoryItemSize.height,
|
||||
itemCount: component.categoryItems.count
|
||||
))
|
||||
} else {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 1,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: stateValue.peers.count
|
||||
))
|
||||
}
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 1,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0),
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: stateValue.peers.count
|
||||
))
|
||||
}
|
||||
|
||||
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||
|
||||
var navigationHeight: CGFloat = 56.0
|
||||
|
||||
let navigationSideInset: CGFloat = 16.0
|
||||
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||
var navigationButtonsWidth: CGFloat = 0.0
|
||||
|
||||
if case .stories = component.stateContext.subject {
|
||||
} else {
|
||||
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: navigationHeight, height: navigationHeight))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||
)
|
||||
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize)
|
||||
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||
if navigationLeftButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationLeftButtonView)
|
||||
}
|
||||
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||
}
|
||||
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
||||
}
|
||||
|
||||
let navigationRightButtonSize = self.navigationRightButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
content: AnyComponent(Text(text: "Done", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
|
||||
component.completion(EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
))
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: navigationHeight, height: navigationHeight))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||
)
|
||||
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize)
|
||||
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||
if navigationLeftButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationLeftButtonView)
|
||||
let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - navigationSideInset - navigationRightButtonSize.width, y: floor((navigationHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
|
||||
if let navigationRightButtonView = self.navigationRightButton.view {
|
||||
if navigationRightButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationRightButtonView)
|
||||
}
|
||||
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||
transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame)
|
||||
}
|
||||
navigationButtonsWidth += navigationRightButtonSize.width + navigationSideInset
|
||||
|
||||
let title: String
|
||||
switch component.stateContext.subject {
|
||||
case .stories:
|
||||
title = "Share Story"
|
||||
case .chats:
|
||||
title = "Send as a Message"
|
||||
case let .contacts(category):
|
||||
switch category {
|
||||
case .closeFriends:
|
||||
title = "Close Friends"
|
||||
case .contacts:
|
||||
title = "Excluded People"
|
||||
case .nobody:
|
||||
title = "Selected Contacts"
|
||||
case .everyone:
|
||||
title = ""
|
||||
}
|
||||
case .search:
|
||||
title = ""
|
||||
}
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "Share Story", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset - navigationLeftButtonFrame.maxX, height: navigationHeight)
|
||||
containerSize: CGSize(width: availableSize.width - navigationButtonsWidth, height: navigationHeight)
|
||||
)
|
||||
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
@@ -943,7 +1033,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 600.0)
|
||||
if case .stories = component.stateContext.subject {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 410.0)
|
||||
} else {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 600.0)
|
||||
}
|
||||
}
|
||||
|
||||
self.navigationBackgroundView.update(size: CGSize(width: availableSize.width, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
|
||||
@@ -951,84 +1045,47 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionButtonTitle: String = "Post Story"
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
actionButtonTitle = "Post Story"
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
actionButtonTitle = "Send to Close Friends"
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
actionButtonTitle = "Send to Contacts"
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
actionButtonTitle = "Send to Selected Contacts"
|
||||
}
|
||||
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: actionButtonTitle,
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: actionButtonTitle,
|
||||
badge: 0,
|
||||
textColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: environment.theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
|
||||
component.completion(EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
))
|
||||
controller.dismiss()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 50.0)
|
||||
)
|
||||
|
||||
var bottomPanelHeight: CGFloat = 0.0
|
||||
if environment.inputHeight != 0.0 {
|
||||
bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height
|
||||
} else {
|
||||
bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||
}
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||
if let actionButtonView = self.actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.addSubview(actionButtonView)
|
||||
if case .stories = component.stateContext.subject {
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(
|
||||
text: "Send as a Message",
|
||||
font: Font.regular(17.0),
|
||||
color: environment.theme.list.itemAccentColor
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
component.secondaryAction()
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: 200.0, height: 44.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 44.0)
|
||||
)
|
||||
|
||||
if environment.inputHeight != 0.0 {
|
||||
bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height
|
||||
} else {
|
||||
bottomPanelHeight += environment.safeInsets.bottom + actionButtonSize.height
|
||||
}
|
||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - actionButtonSize.width) / 2.0, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||
if let actionButtonView = self.actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.addSubview(actionButtonView)
|
||||
}
|
||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0)))
|
||||
self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0)))
|
||||
self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: bottomPanelHeight + environment.safeInsets.bottom, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections)
|
||||
let previousItemLayout = self.itemLayout
|
||||
self.itemLayout = itemLayout
|
||||
@@ -1099,13 +1156,16 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public final class StateContext {
|
||||
public enum Subject: Equatable {
|
||||
case contacts
|
||||
case stories
|
||||
case chats
|
||||
case contacts(EngineStoryPrivacy.Base)
|
||||
case search(String)
|
||||
}
|
||||
|
||||
fileprivate var stateValue: State?
|
||||
|
||||
public let subject: Subject
|
||||
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
private let stateSubject = Promise<State>()
|
||||
@@ -1119,12 +1179,51 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Subject = .contacts
|
||||
subject: Subject = .chats,
|
||||
initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||
) {
|
||||
self.subject = subject
|
||||
self.initialPeerIds = initialPeerIds
|
||||
|
||||
switch subject {
|
||||
case .contacts:
|
||||
case .stories:
|
||||
let state = State(peers: [], presences: [:])
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
self.readySubject.set(true)
|
||||
case .chats:
|
||||
self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] chatList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var selectedPeers: [EnginePeer] = []
|
||||
for item in chatList.items.reversed() {
|
||||
if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}
|
||||
|
||||
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||
for item in chatList.items {
|
||||
presences[item.renderedPeer.peerId] = item.presence
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
peers = chatList.items.filter { !self.initialPeerIds.contains($0.renderedPeer.peerId) }.reversed().compactMap { $0.renderedPeer.peer }
|
||||
peers.insert(contentsOf: selectedPeers, at: 0)
|
||||
|
||||
let state = State(
|
||||
peers: peers,
|
||||
presences: presences
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case let .contacts(base):
|
||||
self.stateDisposable = (context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
|
||||
)
|
||||
@@ -1133,23 +1232,40 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
let state = State(
|
||||
peers: contactList.peers.sorted(by: { lhs, rhs in
|
||||
let lhsPresence = contactList.presences[lhs.id]
|
||||
let rhsPresence = contactList.presences[rhs.id]
|
||||
|
||||
if let lhsPresence, let rhsPresence {
|
||||
return lhsPresence.status > rhsPresence.status
|
||||
} else if lhsPresence != nil {
|
||||
return true
|
||||
} else if rhsPresence != nil {
|
||||
return false
|
||||
} else {
|
||||
return lhs.id < rhs.id
|
||||
var selectedPeers: [EnginePeer] = []
|
||||
if case .closeFriends = base {
|
||||
for peer in contactList.peers {
|
||||
if case let .user(user) = peer, user.flags.contains(.isCloseFriend) {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}),
|
||||
}
|
||||
selectedPeers = selectedPeers.sorted(by: { lhs, rhs in
|
||||
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
|
||||
if result == .orderedSame {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return result == .orderedAscending
|
||||
}
|
||||
})
|
||||
self.initialPeerIds = Set(selectedPeers.map { $0.id })
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) }.sorted(by: { lhs, rhs in
|
||||
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
|
||||
if result == .orderedSame {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return result == .orderedAscending
|
||||
}
|
||||
})
|
||||
peers.insert(contentsOf: selectedPeers, at: 0)
|
||||
|
||||
let state = State(
|
||||
peers: peers,
|
||||
presences: contactList.presences
|
||||
)
|
||||
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
@@ -1183,45 +1299,49 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||
public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void) {
|
||||
self.context = context
|
||||
|
||||
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .everyone,
|
||||
title: "Everyone",
|
||||
icon: "Chat List/Filters/Channel",
|
||||
iconColor: .blue,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .contacts,
|
||||
title: "Contacts",
|
||||
icon: "Chat List/Tabs/IconContacts",
|
||||
iconColor: .yellow,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .closeFriends,
|
||||
title: "Close Friends",
|
||||
icon: "Call/StarHighlighted",
|
||||
iconColor: .green,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .selectedContacts,
|
||||
title: "Selected Contacts",
|
||||
icon: "Chat List/Filters/Group",
|
||||
iconColor: .purple,
|
||||
actionTitle: nil
|
||||
))
|
||||
if case .stories = stateContext.subject {
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .everyone,
|
||||
title: "Everyone",
|
||||
icon: "Chat List/Filters/Channel",
|
||||
iconColor: .blue,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .contacts,
|
||||
title: "Contacts",
|
||||
icon: "Chat List/Tabs/IconContacts",
|
||||
iconColor: .yellow,
|
||||
actionTitle: "exclude people"
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .closeFriends,
|
||||
title: "Close Friends",
|
||||
icon: "Call/StarHighlighted",
|
||||
iconColor: .green,
|
||||
actionTitle: "edit list"
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .selectedContacts,
|
||||
title: "Selected Contacts",
|
||||
icon: "Chat List/Filters/Group",
|
||||
iconColor: .violet,
|
||||
actionTitle: "edit list"
|
||||
))
|
||||
}
|
||||
|
||||
super.init(context: context, component: ShareWithPeersScreenComponent(
|
||||
context: context,
|
||||
stateContext: stateContext,
|
||||
initialPrivacy: initialPrivacy,
|
||||
categoryItems: categoryItems,
|
||||
completion: completion
|
||||
completion: completion,
|
||||
editCategory: editCategory,
|
||||
secondaryAction: secondaryAction
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ public final class TextFieldComponent: Component {
|
||||
self.layoutManager.ensureLayout(for: self.textContainer)
|
||||
|
||||
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
|
||||
let size = CGSize(width: availableSize.width, height: min(100.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
|
||||
let refreshScrolling = self.textView.bounds.size != size
|
||||
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "off shadow.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+1366
File diff suppressed because it is too large
Load Diff
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "SubtitleArrow.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
%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 4.664993 10.125000 cm
|
||||
0.000000 0.478431 1.000000 scn
|
||||
4.470226 8.989735 m
|
||||
4.729925 9.249434 4.729925 9.670488 4.470226 9.930187 c
|
||||
4.210527 10.189886 3.789473 10.189886 3.529774 9.930187 c
|
||||
4.470226 8.989735 l
|
||||
h
|
||||
0.000000 5.459961 m
|
||||
-0.470226 5.930187 l
|
||||
-0.729925 5.670488 -0.729925 5.249434 -0.470226 4.989735 c
|
||||
0.000000 5.459961 l
|
||||
h
|
||||
3.529774 0.989735 m
|
||||
3.789473 0.730036 4.210527 0.730036 4.470226 0.989735 c
|
||||
4.729925 1.249434 4.729925 1.670488 4.470226 1.930187 c
|
||||
3.529774 0.989735 l
|
||||
h
|
||||
3.529774 9.930187 m
|
||||
-0.470226 5.930187 l
|
||||
0.470226 4.989735 l
|
||||
4.470226 8.989735 l
|
||||
3.529774 9.930187 l
|
||||
h
|
||||
-0.470226 4.989735 m
|
||||
3.529774 0.989735 l
|
||||
4.470226 1.930187 l
|
||||
0.470226 5.930187 l
|
||||
-0.470226 4.989735 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
767
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 5.329987 9.330078 ]
|
||||
/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
|
||||
0000000857 00000 n
|
||||
0000000879 00000 n
|
||||
0000001050 00000 n
|
||||
0000001124 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1183
|
||||
%%EOF
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "check.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
%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 40.000000 40.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
16.557842 15.662290 m
|
||||
17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c
|
||||
8.443170 0.821279 l
|
||||
8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c
|
||||
7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c
|
||||
0.389548 6.608668 l
|
||||
-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c
|
||||
0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c
|
||||
7.112782 3.647242 l
|
||||
14.716830 15.276961 l
|
||||
15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
584
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 4 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.000000 20.000000 m
|
||||
0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c
|
||||
20.000000 40.000000 l
|
||||
31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c
|
||||
40.000000 20.000000 l
|
||||
40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c
|
||||
20.000000 0.000000 l
|
||||
8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c
|
||||
0.000000 20.000000 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
472
|
||||
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
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
7 0 obj
|
||||
46
|
||||
endobj
|
||||
|
||||
8 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 40.000000 40.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
|
||||
0000000842 00000 n
|
||||
0000000864 00000 n
|
||||
0000001584 00000 n
|
||||
0000001606 00000 n
|
||||
0000001904 00000 n
|
||||
0000002006 00000 n
|
||||
0000002027 00000 n
|
||||
0000002200 00000 n
|
||||
0000002274 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 10 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
2334
|
||||
%%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) -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, completion: completion)
|
||||
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed)
|
||||
}
|
||||
|
||||
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
|
||||
|
||||
@@ -26,6 +26,7 @@ import AvatarNode
|
||||
import LocalMediaResources
|
||||
import ShareWithPeersScreen
|
||||
import ImageCompression
|
||||
import TextFormat
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
@@ -196,16 +197,16 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var transitionIn: StoryCameraTransitionIn?
|
||||
if let cameraItemView = self.rootTabController?.viewForCameraItem() {
|
||||
transitionIn = StoryCameraTransitionIn(
|
||||
sourceView: cameraItemView,
|
||||
sourceRect: cameraItemView.bounds,
|
||||
sourceCornerRadius: cameraItemView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
self.openStoryCamera(
|
||||
transitionIn: transitionIn,
|
||||
let coordinator = self.openStoryCamera(
|
||||
transitionIn: nil,
|
||||
transitionedIn: { [weak self] in
|
||||
guard let self, let rootTabController = self.rootTabController else {
|
||||
return
|
||||
}
|
||||
if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
|
||||
rootTabController.selectedIndex = index
|
||||
}
|
||||
},
|
||||
transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
@@ -218,18 +219,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
destinationCornerRadius: transitionView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if let cameraItemView = self.rootTabController?.viewForCameraItem() {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: cameraItemView,
|
||||
destinationRect: cameraItemView.bounds,
|
||||
destinationCornerRadius: cameraItemView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -288,9 +282,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller)
|
||||
}
|
||||
|
||||
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) {
|
||||
@discardableResult
|
||||
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? {
|
||||
guard let controller = self.viewControllers.last as? ViewController else {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
controller.view.endEditing(true)
|
||||
|
||||
@@ -299,7 +294,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var returnToCameraImpl: (() -> Void)?
|
||||
var dismissCameraImpl: (() -> Void)?
|
||||
var hideCameraImpl: (() -> Void)?
|
||||
var showDraftTooltipImpl: (() -> Void)?
|
||||
let cameraController = CameraScreen(
|
||||
context: context,
|
||||
@@ -326,7 +320,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
}
|
||||
},
|
||||
completion: { result in
|
||||
completion: { result, resultTransition in
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreen.Subject? in
|
||||
switch value {
|
||||
@@ -342,17 +336,37 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return .draft(draft)
|
||||
}
|
||||
}
|
||||
|
||||
var transitionIn: MediaEditorScreen.TransitionIn?
|
||||
if let resultTransition, let sourceView = resultTransition.sourceView {
|
||||
transitionIn = .gallery(
|
||||
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
|
||||
sourceView: sourceView,
|
||||
sourceRect: resultTransition.sourceRect,
|
||||
sourceImage: resultTransition.sourceImage
|
||||
)
|
||||
)
|
||||
} else {
|
||||
transitionIn = .camera
|
||||
}
|
||||
|
||||
let controller = MediaEditorScreen(
|
||||
context: context,
|
||||
subject: subject,
|
||||
transitionIn: nil,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { finished in
|
||||
if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView {
|
||||
if finished, let transitionOut = transitionOut(finished), let destinationView = transitionOut.destinationView {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: transitionOut.destinationRect,
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius
|
||||
)
|
||||
} else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut() {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: destinationRect,
|
||||
destinationCornerRadius: 0.0
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@@ -364,13 +378,75 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
|
||||
if let chatListController = self.chatListController as? ChatListControllerImpl {
|
||||
chatListController.scrollToTop?()
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy).start()
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, _):
|
||||
let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start()
|
||||
Queue.mainQueue().after(0.3, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
case let .message(peerIds, timeout):
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpg"
|
||||
let _ = try? imageData.write(to: URL(fileURLWithPath: tempFilePath))
|
||||
|
||||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
let imageFlags: TelegramMediaImageFlags = []
|
||||
// var stickerFiles: [TelegramMediaFile] = []
|
||||
// if !stickers.isEmpty {
|
||||
// for fileReference in stickers {
|
||||
// stickerFiles.append(fileReference.media)
|
||||
// }
|
||||
// }
|
||||
// if !stickerFiles.isEmpty {
|
||||
// attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles))
|
||||
// imageFlags.insert(.hasStickers)
|
||||
// }
|
||||
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags)
|
||||
if let timeout, timeout > 0 && timeout <= 60 {
|
||||
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: nil))
|
||||
}
|
||||
|
||||
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:]
|
||||
text.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: text.length), using: { value, _, _ in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let file = value.file {
|
||||
if let packId = value.interactivelySelectedFromPackId {
|
||||
bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
|
||||
for entity in entities {
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
if let packId = bubbleUpEmojiOrStickersetsById[fileId] {
|
||||
if !bubbleUpEmojiOrStickersets.contains(packId) {
|
||||
bubbleUpEmojiOrStickersets.append(packId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = enqueueMessagesToMultiplePeers(
|
||||
account: self.context.account,
|
||||
peerIds: peerIds, threadIds: [:],
|
||||
messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start()
|
||||
}
|
||||
}
|
||||
case let .video(content, _, values, duration, dimensions, caption):
|
||||
let adjustments: VideoMediaResourceAdjustments
|
||||
@@ -388,31 +464,34 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(localIdentifier):
|
||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||
}
|
||||
let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy).start()
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
if case let .story(storyPrivacy, _) = privacy {
|
||||
let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start()
|
||||
Queue.mainQueue().after(0.3, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismissCameraImpl?()
|
||||
commit()
|
||||
Queue.mainQueue().after(0.1) {
|
||||
commit()
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.sourceHint = .camera
|
||||
controller.cancelled = { showDraftTooltip in
|
||||
if showDraftTooltip {
|
||||
showDraftTooltipImpl?()
|
||||
}
|
||||
returnToCameraImpl?()
|
||||
}
|
||||
controller.onReady = {
|
||||
hideCameraImpl?()
|
||||
}
|
||||
presentImpl?(controller)
|
||||
}
|
||||
)
|
||||
cameraController.transitionedIn = transitionedIn
|
||||
controller.push(cameraController)
|
||||
presentImpl = { [weak cameraController] c in
|
||||
if let navigationController = cameraController?.navigationController as? NavigationController {
|
||||
@@ -429,16 +508,28 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
cameraController.returnFromEditor()
|
||||
}
|
||||
}
|
||||
hideCameraImpl = { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.commitTransitionToEditor()
|
||||
}
|
||||
}
|
||||
showDraftTooltipImpl = { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.presentDraftTooltip()
|
||||
}
|
||||
}
|
||||
return StoryCameraTransitionInCoordinator(
|
||||
animateIn: { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.updateTransitionProgress(0.0, transition: .immediate)
|
||||
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false)
|
||||
}
|
||||
},
|
||||
updateTransitionProgress: { [weak cameraController] transitionFraction in
|
||||
if let cameraController {
|
||||
cameraController.updateTransitionProgress(transitionFraction, transition: .immediate)
|
||||
}
|
||||
},
|
||||
completeWithTransitionProgressAndVelocity: { [weak cameraController] transitionFraction, velocity in
|
||||
if let cameraController {
|
||||
cameraController.completeWithTransitionProgress(transitionFraction, velocity: velocity, dismissing: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func openSettings() {
|
||||
|
||||
Reference in New Issue
Block a user