Merge commit 'f11c45070e5e533f6f4077cc9f7708afd443538b' into HEAD

This commit is contained in:
Ali
2023-05-29 23:10:08 +04:00
74 changed files with 5557 additions and 2039 deletions
@@ -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
+1
View File
@@ -57,6 +57,7 @@ swift_library(
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/ImageBlur:ImageBlur",
"//submodules/TelegramCore:TelegramCore",
],
visibility = [
"//visibility:public",
+33 -11
View File
@@ -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)
}
}
}
+31 -4
View File
@@ -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:
+11 -15
View File
@@ -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 {
+428 -171
View File
@@ -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
+1
View File
@@ -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)
@@ -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
}
@@ -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!
}
}
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 {
@@ -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
@@ -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))
@@ -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",
@@ -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
@@ -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
@@ -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)
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "off shadow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "SubtitleArrow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -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
@@ -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() {