diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 1741d7371c..99d22b5afb 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -4,19 +4,107 @@ import SwiftSignalKit import AVFoundation import CoreImage +final class CameraSession { + private let singleSession: AVCaptureSession? + private let multiSession: Any? + + init() { + if #available(iOS 13.0, *) { + self.multiSession = AVCaptureMultiCamSession() + self.singleSession = nil + } else { + self.singleSession = AVCaptureSession() + self.multiSession = nil + } + } + + var session: AVCaptureSession { + if #available(iOS 13.0, *), let multiSession = self.multiSession as? AVCaptureMultiCamSession { + return multiSession + } else if let session = self.singleSession { + return session + } else { + fatalError() + } + } + + var supportsDualCam: Bool { + return self.multiSession != nil + } +} + +final class CameraDeviceContext { + private weak var session: CameraSession? + private weak var previewView: CameraSimplePreviewView? + + let device = CameraDevice() + let input = CameraInput() + let output = CameraOutput() + + init(session: CameraSession) { + self.session = session + } + + func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) { + guard let session = self.session else { + return + } + + self.previewView = previewView + + self.device.configure(for: session, position: position) + self.input.configure(for: session, device: self.device, audio: audio) + self.output.configure(for: session, device: self.device, input: self.input, previewView: previewView, audio: audio, photo: photo, metadata: metadata) + + self.device.configureDeviceFormat(maxDimensions: self.preferredMaxDimensions, maxFramerate: self.preferredMaxFrameRate) + self.output.configureVideoStabilization() + } + + func switchOutputWith(_ otherContext: CameraDeviceContext) { +// guard let session = self.session else { +// return +// } +// self.output.reconfigure(for: session, device: self.device, input: self.input, otherPreviewView: otherContext.previewView, otherOutput: otherContext.output) +// otherContext.output.reconfigure(for: session, device: otherContext.device, input: otherContext.input, otherPreviewView: self.previewView, otherOutput: self.output) + } + + func invalidate() { + guard let session = self.session else { + return + } + self.output.invalidate(for: session) + self.input.invalidate(for: session) + } + + private var preferredMaxDimensions: CMVideoDimensions { + return CMVideoDimensions(width: 1920, height: 1080) + } + + private var preferredMaxFrameRate: Double { + switch DeviceModel.current { + case .iPhone14ProMax, .iPhone13ProMax: + return 60.0 + default: + return 30.0 + } + } +} + private final class CameraContext { private let queue: Queue - private let session: AVCaptureSession - private let device: CameraDevice - private let input = CameraInput() - private let output = CameraOutput() + + private let session: CameraSession + + private let mainDeviceContext: CameraDeviceContext + private var additionalDeviceContext: CameraDeviceContext? + private let cameraImageContext = CIContext() private let initialConfiguration: Camera.Configuration private var invalidated = false private let detectedCodesPipe = ValuePipe<[CameraCode]>() - fileprivate let changingPositionPromise = ValuePromise(false) + fileprivate let modeChangePromise = ValuePromise(.none) var previewNode: CameraPreviewNode? { didSet { @@ -34,15 +122,18 @@ private final class CameraContext { didSet { if let oldValue { Queue.mainQueue().async { - oldValue.session = nil - self.simplePreviewView?.session = self.session + oldValue.invalidate() + self.simplePreviewView?.setSession(self.session.session, autoConnect: true) } } } } + var secondaryPreviewView: CameraSimplePreviewView? + private var lastSnapshotTimestamp: Double = CACurrentMediaTime() - private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) { + private var lastAdditionalSnapshotTimestamp: Double = CACurrentMediaTime() + private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool, additional: Bool) { Queue.concurrentDefaultQueue().async { var ciImage = CIImage(cvImageBuffer: pixelBuffer) let size = ciImage.extent.size @@ -53,30 +144,30 @@ private final class CameraContext { } ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) { - let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) - CameraSimplePreviewView.saveLastStateImage(uiImage) + let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: additional ? .up : .right) + if additional { + CameraSimplePreviewView.saveAdditionalLastStateImage(uiImage) + } else { + CameraSimplePreviewView.saveLastStateImage(uiImage) + } } } } private var videoOrientation: AVCaptureVideoOrientation? - init(queue: Queue, session: AVCaptureSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?) { + init(queue: Queue, session: CameraSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?, secondaryPreviewView: CameraSimplePreviewView?) { self.queue = queue self.session = session self.initialConfiguration = configuration self.simplePreviewView = previewView + self.secondaryPreviewView = secondaryPreviewView - self.device = CameraDevice() + self.mainDeviceContext = CameraDeviceContext(session: session) self.configure { - self.device.configure(for: self.session, position: configuration.position) - 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: self.preferredMaxFrameRate) - self.output.configureVideoStabilization() + self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata) } - self.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in + self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self else { return } @@ -88,12 +179,12 @@ private final class CameraContext { if #available(iOS 13.0, *) { mirror = connection.inputPorts.first?.sourceDevicePosition == .front } - self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) self.lastSnapshotTimestamp = timestamp } } - self.output.processFaceLandmarks = { [weak self] observations in + self.mainDeviceContext.output.processFaceLandmarks = { [weak self] observations in guard let self else { return } @@ -102,36 +193,26 @@ private final class CameraContext { } } - self.output.processCodes = { [weak self] codes in + self.mainDeviceContext.output.processCodes = { [weak self] codes in self?.detectedCodesPipe.putNext(codes) } } - - private var preferredMaxFrameRate: Double { - switch DeviceModel.current { - case .iPhone14ProMax, .iPhone13ProMax: - return 60.0 - default: - return 30.0 - } - } - + func startCapture() { - guard !self.session.isRunning else { + guard !self.session.session.isRunning else { return } - self.session.startRunning() + self.session.session.startRunning() } func stopCapture(invalidate: Bool = false) { if invalidate { self.configure { - self.input.invalidate(for: self.session) - self.output.invalidate(for: self.session) + self.mainDeviceContext.invalidate() } } - self.session.stopRunning() + self.session.session.stopRunning() } func focus(at point: CGPoint, autoFocus: Bool) { @@ -144,17 +225,17 @@ private final class CameraContext { focusMode = .autoFocus exposureMode = .autoExpose } - self.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true) + self.mainDeviceContext.device.setFocusPoint(point, focusMode: focusMode, exposureMode: exposureMode, monitorSubjectAreaChange: true) } func setFps(_ fps: Float64) { - self.device.fps = fps + self.mainDeviceContext.device.fps = fps } - private var changingPosition = false { + private var modeChange: Camera.ModeChange = .none { didSet { - if oldValue != self.changingPosition { - self.changingPositionPromise.set(self.changingPosition) + if oldValue != self.modeChange { + self.modeChangePromise.set(self.modeChange) } } } @@ -164,54 +245,112 @@ private final class CameraContext { return self._positionPromise.get() } + private var tmpPosition: Camera.Position = .back func togglePosition() { - self.configure { - self.input.invalidate(for: self.session) + if self.isDualCamEnabled { let targetPosition: Camera.Position - if case .back = self.device.position { + if case .back = self.tmpPosition { targetPosition = .front } else { targetPosition = .back } + self.tmpPosition = targetPosition self._positionPromise.set(targetPosition) - 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: self.preferredMaxFrameRate) - self.output.configureVideoStabilization() - self.queue.after(0.5) { - self.changingPosition = false +// if let additionalDeviceContext = self.additionalDeviceContext { +// self.mainDeviceContext.switchOutputWith(additionalDeviceContext) +// } + } else { + self.configure { + self.mainDeviceContext.invalidate() + + let targetPosition: Camera.Position + if case .back = self.mainDeviceContext.device.position { + targetPosition = .front + } else { + targetPosition = .back + } + self._positionPromise.set(targetPosition) + self.modeChange = .position + + self.mainDeviceContext.configure(position: targetPosition, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) + + self.queue.after(0.5) { + self.modeChange = .none + } } } } public func setPosition(_ position: Camera.Position) { self.configure { + self.mainDeviceContext.invalidate() + self._positionPromise.set(position) - 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: self.preferredMaxFrameRate) - self.output.configureVideoStabilization() + self.modeChange = .position + + self.mainDeviceContext.configure(position: position, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) + + self.queue.after(0.5) { + self.modeChange = .none + } + } + } + + private var isDualCamEnabled = false + public func setDualCamEnabled(_ enabled: Bool) { + guard enabled != self.isDualCamEnabled else { + return + } + self.isDualCamEnabled = enabled + + self.modeChange = .dualCamera + if enabled { + self.configure { + self.additionalDeviceContext = CameraDeviceContext(session: self.session) + self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false) + } + self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in + guard let self else { + return + } + let timestamp = CACurrentMediaTime() + if timestamp > self.lastAdditionalSnapshotTimestamp + 2.5 { + var mirror = false + if #available(iOS 13.0, *) { + mirror = connection.inputPorts.first?.sourceDevicePosition == .front + } + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: true) + self.lastAdditionalSnapshotTimestamp = timestamp + } + } + } else { + self.configure { + self.additionalDeviceContext?.invalidate() + self.additionalDeviceContext = nil + } + } + + self.queue.after(0.5) { + self.modeChange = .none } } private func configure(_ f: () -> Void) { - self.session.beginConfiguration() + self.session.session.beginConfiguration() f() - self.session.commitConfiguration() + self.session.session.commitConfiguration() } var hasTorch: Signal { - return self.device.isTorchAvailable + return self.mainDeviceContext.device.isTorchAvailable } func setTorchActive(_ active: Bool) { - self.device.setTorchActive(active) + self.mainDeviceContext.device.setTorchActive(active) } var isFlashActive: Signal { - return self.output.isFlashActive + return self.mainDeviceContext.output.isFlashActive } private var _flashMode: Camera.FlashMode = .off { @@ -229,23 +368,37 @@ private final class CameraContext { } func setZoomLevel(_ zoomLevel: CGFloat) { - self.device.setZoomLevel(zoomLevel) + self.mainDeviceContext.device.setZoomLevel(zoomLevel) } func setZoomDelta(_ zoomDelta: CGFloat) { - self.device.setZoomDelta(zoomDelta) + self.mainDeviceContext.device.setZoomDelta(zoomDelta) } func takePhoto() -> Signal { - return self.output.takePhoto(orientation: self.videoOrientation ?? .portrait, flashMode: self._flashMode) + let orientation = self.videoOrientation ?? .portrait + if let additionalDeviceContext = self.additionalDeviceContext { + return combineLatest( + self.mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode), + additionalDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode) + ) |> map { main, additional in + if case let .finished(mainImage, _, _) = main, case let .finished(additionalImage, _, _) = additional { + return .finished(mainImage, additionalImage, CACurrentMediaTime()) + } else { + return .began + } + } |> distinctUntilChanged + } else { + return self.mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode) + } } public func startRecording() -> Signal { - return self.output.startRecording() + return self.mainDeviceContext.output.startRecording() } public func stopRecording() -> Signal<(String, UIImage?)?, NoError> { - return self.output.stopRecording() + return self.mainDeviceContext.output.stopRecording() } var detectedCodes: Signal<[CameraCode], NoError> { @@ -285,19 +438,22 @@ public final class Camera { public let metrics: Camera.Metrics - public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false, preferredFps: 60.0), previewView: CameraSimplePreviewView? = nil) { + public init(configuration: Camera.Configuration = Configuration(preset: .hd1920x1080, position: .back, audio: true, photo: false, metadata: false, preferredFps: 60.0), previewView: CameraSimplePreviewView? = nil, secondaryPreviewView: CameraSimplePreviewView? = nil) { self.metrics = Camera.Metrics(model: DeviceModel.current) - let session = AVCaptureSession() - session.usesApplicationAudioSession = true - session.automaticallyConfiguresApplicationAudioSession = false - session.automaticallyConfiguresCaptureDeviceForWideColor = false + let session = CameraSession() + session.session.usesApplicationAudioSession = true + session.session.automaticallyConfiguresApplicationAudioSession = false + session.session.automaticallyConfiguresCaptureDeviceForWideColor = false if let previewView { - previewView.session = session + previewView.setSession(session.session, autoConnect: false) + } + if let secondaryPreviewView { + secondaryPreviewView.setSession(session.session, autoConnect: false) } self.queue.async { - let context = CameraContext(queue: self.queue, session: session, configuration: configuration, metrics: self.metrics, previewView: previewView) + let context = CameraContext(queue: self.queue, session: session, configuration: configuration, metrics: self.metrics, previewView: previewView, secondaryPreviewView: secondaryPreviewView) self.contextRef = Unmanaged.passRetained(context) } } @@ -363,6 +519,14 @@ public final class Camera { } } + public func setDualCamEnabled(_ enabled: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setDualCamEnabled(enabled) + } + } + } + public func takePhoto() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -565,12 +729,17 @@ public final class Camera { } } - public var changingPosition: Signal { + public enum ModeChange: Equatable { + case none + case position + case dualCamera + } + public var modeChange: Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { - disposable.set(context.changingPositionPromise.get().start(next: { value in + disposable.set(context.modeChangePromise.get().start(next: { value in subscriber.putNext(value) })) } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 3df0b04f8a..6839841480 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -29,7 +29,7 @@ final class CameraDevice { public private(set) var audioDevice: AVCaptureDevice? = nil - func configure(for session: AVCaptureSession, position: Camera.Position) { + func configure(for session: CameraSession, position: Camera.Position) { self.position = position if let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first { self.videoDevice = videoDevice diff --git a/submodules/Camera/Sources/CameraInput.swift b/submodules/Camera/Sources/CameraInput.swift index f60d0edec7..58c0f2de0f 100644 --- a/submodules/Camera/Sources/CameraInput.swift +++ b/submodules/Camera/Sources/CameraInput.swift @@ -1,10 +1,10 @@ import AVFoundation class CameraInput { - private var videoInput: AVCaptureDeviceInput? + var videoInput: AVCaptureDeviceInput? private var audioInput: AVCaptureDeviceInput? - func configure(for session: AVCaptureSession, device: CameraDevice, audio: Bool) { + func configure(for session: CameraSession, device: CameraDevice, audio: Bool) { if let videoDevice = device.videoDevice { self.configureVideoInput(for: session, device: videoDevice) } @@ -13,34 +13,34 @@ class CameraInput { } } - func invalidate(for session: AVCaptureSession) { - for input in session.inputs { - session.removeInput(input) + func invalidate(for session: CameraSession) { + for input in session.session.inputs { + session.session.removeInput(input) } } - private func configureVideoInput(for session: AVCaptureSession, device: AVCaptureDevice) { + private func configureVideoInput(for session: CameraSession, device: AVCaptureDevice) { if let currentVideoInput = self.videoInput { - session.removeInput(currentVideoInput) + session.session.removeInput(currentVideoInput) self.videoInput = nil } if let videoInput = try? AVCaptureDeviceInput(device: device) { self.videoInput = videoInput - if session.canAddInput(videoInput) { - session.addInput(videoInput) + if session.session.canAddInput(videoInput) { + session.session.addInputWithNoConnections(videoInput) } } } - private func configureAudioInput(for session: AVCaptureSession, device: AVCaptureDevice) { + private func configureAudioInput(for session: CameraSession, device: AVCaptureDevice) { if let currentAudioInput = self.audioInput { - session.removeInput(currentAudioInput) + session.session.removeInput(currentAudioInput) self.audioInput = nil } if let audioInput = try? AVCaptureDeviceInput(device: device) { self.audioInput = audioInput - if session.canAddInput(audioInput) { - session.addInput(audioInput) + if session.session.canAddInput(audioInput) { + session.session.addInput(audioInput) } } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 18b1c963b9..967bdb3246 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -45,12 +45,16 @@ public struct CameraCode: Equatable { } final class CameraOutput: NSObject { - private let photoOutput = AVCapturePhotoOutput() - private let videoOutput = AVCaptureVideoDataOutput() - private let audioOutput = AVCaptureAudioDataOutput() - private let metadataOutput = AVCaptureMetadataOutput() + let photoOutput = AVCapturePhotoOutput() + let videoOutput = AVCaptureVideoDataOutput() + let audioOutput = AVCaptureAudioDataOutput() + let metadataOutput = AVCaptureMetadataOutput() private let faceLandmarksOutput = FaceLandmarksDataOutput() + private var photoConnection: AVCaptureConnection? + private var videoConnection: AVCaptureConnection? + private var previewConnection: AVCaptureConnection? + private let queue = DispatchQueue(label: "") private let metadataQueue = DispatchQueue(label: "") private let faceLandmarksQueue = DispatchQueue(label: "") @@ -83,31 +87,136 @@ final class CameraOutput: NSObject { self.audioOutput.setSampleBufferDelegate(nil, queue: nil) } - func configure(for session: AVCaptureSession, configuration: Camera.Configuration) { - if session.canAddOutput(self.videoOutput) { - session.addOutput(self.videoOutput) + func configure(for session: CameraSession, device: CameraDevice, input: CameraInput, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) { + if session.session.canAddOutput(self.videoOutput) { + session.session.addOutputWithNoConnections(self.videoOutput) self.videoOutput.setSampleBufferDelegate(self, queue: self.queue) } - if configuration.audio, session.canAddOutput(self.audioOutput) { - session.addOutput(self.audioOutput) + if audio, session.session.canAddOutput(self.audioOutput) { + session.session.addOutput(self.audioOutput) self.audioOutput.setSampleBufferDelegate(self, queue: self.queue) } - if configuration.photo, session.canAddOutput(self.photoOutput) { - session.addOutput(self.photoOutput) + if photo, session.session.canAddOutput(self.photoOutput) { + session.session.addOutputWithNoConnections(self.photoOutput) } - if configuration.metadata, session.canAddOutput(self.metadataOutput) { - session.addOutput(self.metadataOutput) + if metadata, session.session.canAddOutput(self.metadataOutput) { + session.session.addOutput(self.metadataOutput) self.metadataOutput.setMetadataObjectsDelegate(self, queue: self.metadataQueue) if self.metadataOutput.availableMetadataObjectTypes.contains(.qr) { self.metadataOutput.metadataObjectTypes = [.qr] } } + + if #available(iOS 13.0, *) { + if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) { + if let previewView { + let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: previewView.videoPreviewLayer) + if session.session.canAddConnection(previewConnection) { + session.session.addConnection(previewConnection) + self.previewConnection = previewConnection + } + } + + let videoConnection = AVCaptureConnection(inputPorts: ports, output: self.videoOutput) + if session.session.canAddConnection(videoConnection) { + session.session.addConnection(videoConnection) + self.videoConnection = videoConnection + } + + if photo { + let photoConnection = AVCaptureConnection(inputPorts: ports, output: self.photoOutput) + if session.session.canAddConnection(photoConnection) { + session.session.addConnection(photoConnection) + self.photoConnection = photoConnection + } + } + } + } } - func invalidate(for session: AVCaptureSession) { - for output in session.outputs { - session.removeOutput(output) + func reconfigure(for session: CameraSession, device: CameraDevice, input: CameraInput, otherPreviewView: CameraSimplePreviewView?, otherOutput: CameraOutput) { + if #available(iOS 13.0, *) { + if let previewConnection = self.previewConnection { + if session.session.connections.contains(where: { $0 === previewConnection }) { + session.session.removeConnection(previewConnection) + } + self.previewConnection = nil + } + if let videoConnection = self.videoConnection { + if session.session.connections.contains(where: { $0 === videoConnection }) { + session.session.removeConnection(videoConnection) + } + self.videoConnection = nil + } + if let photoConnection = self.photoConnection { + if session.session.connections.contains(where: { $0 === photoConnection }) { + session.session.removeConnection(photoConnection) + } + self.photoConnection = nil + } + + if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) { + if let otherPreviewView { + let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: otherPreviewView.videoPreviewLayer) + if session.session.canAddConnection(previewConnection) { + session.session.addConnection(previewConnection) + self.previewConnection = previewConnection + } + } + + let videoConnection = AVCaptureConnection(inputPorts: ports, output: otherOutput.videoOutput) + if session.session.canAddConnection(videoConnection) { + session.session.addConnection(videoConnection) + self.videoConnection = videoConnection + } + + + let photoConnection = AVCaptureConnection(inputPorts: ports, output: otherOutput.photoOutput) + if session.session.canAddConnection(photoConnection) { + session.session.addConnection(photoConnection) + self.photoConnection = photoConnection + } + } + } + } + + func toggleConnection() { + + } + + func invalidate(for session: CameraSession) { + if #available(iOS 13.0, *) { + if let previewConnection = self.previewConnection { + if session.session.connections.contains(where: { $0 === previewConnection }) { + session.session.removeConnection(previewConnection) + } + self.previewConnection = nil + } + if let videoConnection = self.videoConnection { + if session.session.connections.contains(where: { $0 === videoConnection }) { + session.session.removeConnection(videoConnection) + } + self.videoConnection = nil + } + if let photoConnection = self.photoConnection { + if session.session.connections.contains(where: { $0 === photoConnection }) { + session.session.removeConnection(photoConnection) + } + self.photoConnection = nil + } + } + if session.session.outputs.contains(where: { $0 === self.videoOutput }) { + session.session.removeOutput(self.videoOutput) + } + if session.session.outputs.contains(where: { $0 === self.audioOutput }) { + session.session.removeOutput(self.audioOutput) + } + if session.session.outputs.contains(where: { $0 === self.photoOutput }) { + session.session.removeOutput(self.photoOutput) + } + if session.session.outputs.contains(where: { $0 === self.metadataOutput }) { + session.session.removeOutput(self.metadataOutput) } } @@ -149,7 +258,7 @@ final class CameraOutput: NSObject { settings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType] } if #available(iOS 13.0, *) { - settings.photoQualityPrioritization = .balanced + settings.photoQualityPrioritization = .speed } let uniqueId = settings.uniqueID diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 279527170f..c68f6d16a9 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -26,12 +26,31 @@ public class CameraSimplePreviewView: UIView { } } + static func lastAdditionalStateImage() -> UIImage { + let imagePath = NSTemporaryDirectory() + "cameraImage2.jpg" + if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) { + return image + } else { + return UIImage(bundleImageName: "Camera/SelfiePlaceholder")! + } + } + + static func saveAdditionalLastStateImage(_ image: UIImage) { + let imagePath = NSTemporaryDirectory() + "cameraImage2.jpg" + if let data = image.jpegData(compressionQuality: 0.6) { + try? data.write(to: URL(fileURLWithPath: imagePath)) + } + } + private var previewingDisposable: Disposable? private let placeholderView = UIImageView() - public override init(frame: CGRect) { + public init(frame: CGRect, additional: Bool) { super.init(frame: frame) - self.placeholderView.image = CameraSimplePreviewView.lastStateImage() + self.videoPreviewLayer.videoGravity = .resizeAspectFill + + self.placeholderView.contentMode = .scaleAspectFill + self.placeholderView.image = additional ? CameraSimplePreviewView.lastAdditionalStateImage() : CameraSimplePreviewView.lastStateImage() self.addSubview(self.placeholderView) if #available(iOS 13.0, *) { @@ -66,19 +85,27 @@ public class CameraSimplePreviewView: UIView { self.placeholderView.frame = self.bounds.insetBy(dx: -1.0, dy: -1.0) } + private var _videoPreviewLayer: AVCaptureVideoPreviewLayer? var videoPreviewLayer: AVCaptureVideoPreviewLayer { - guard let layer = layer as? AVCaptureVideoPreviewLayer else { + if let layer = self._videoPreviewLayer { + return layer + } + guard let layer = self.layer as? AVCaptureVideoPreviewLayer else { fatalError() } + self._videoPreviewLayer = layer return layer } - var session: AVCaptureSession? { - get { - return self.videoPreviewLayer.session - } - set { - self.videoPreviewLayer.session = newValue + func invalidate() { + self.videoPreviewLayer.session = nil + } + + func setSession(_ session: AVCaptureSession, autoConnect: Bool) { + if autoConnect { + self.videoPreviewLayer.session = session + } else { + self.videoPreviewLayer.setSessionWithNoConnection(session) } } diff --git a/submodules/Camera/Sources/PhotoCaptureContext.swift b/submodules/Camera/Sources/PhotoCaptureContext.swift index 29b569193a..747f1b5400 100644 --- a/submodules/Camera/Sources/PhotoCaptureContext.swift +++ b/submodules/Camera/Sources/PhotoCaptureContext.swift @@ -3,10 +3,33 @@ import AVFoundation import UIKit import SwiftSignalKit -public enum PhotoCaptureResult { +public enum PhotoCaptureResult: Equatable { case began - case finished(UIImage) + case finished(UIImage, UIImage?, Double) case failed + + public static func == (lhs: PhotoCaptureResult, rhs: PhotoCaptureResult) -> Bool { + switch lhs { + case .began: + if case .began = rhs { + return true + } else { + return false + } + case .failed: + if case .failed = rhs { + return true + } else { + return false + } + case let .finished(_, _, lhsTime): + if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime { + return true + } else { + return false + } + } + } } final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate { @@ -62,7 +85,7 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate { } UIGraphicsEndImageContext() } - self.pipe.putNext(.finished(image)) + self.pipe.putNext(.finished(image, nil, CACurrentMediaTime())) } else { self.pipe.putNext(.failed) } diff --git a/submodules/Display/Source/Navigation/NavigationContainer.swift b/submodules/Display/Source/Navigation/NavigationContainer.swift index c6a3a7f82c..e317248156 100644 --- a/submodules/Display/Source/Navigation/NavigationContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationContainer.swift @@ -507,6 +507,7 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega let displayNode = topTransition.previous.value.displayNode displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak displayNode] _ in displayNode?.removeFromSupernode() + displayNode?.layer.removeAllAnimations() }) } else { topTransition.previous.value.setIgnoreAppearanceMethodInvocations(true) diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index c7e28e21ee..2645d75eae 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -334,68 +334,68 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } view.containerView = self - let processSnap: (Bool, UIView) -> Void = { [weak self] snapped, snapView in - guard let self else { - return - } - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - if snapped { - self.insertSubview(snapView, belowSubview: view) - if snapView.alpha < 1.0 { - self.hapticFeedback.impact(.light) - } - transition.updateAlpha(layer: snapView.layer, alpha: 1.0) - } else { - transition.updateAlpha(layer: snapView.layer, alpha: 0.0) - } - } - - view.onSnapUpdated = { [weak self, weak view] type, snapped in - guard let self else { - return - } - switch type { - case .centerX: - processSnap(snapped, self.xAxisView) - case .centerY: - processSnap(snapped, self.yAxisView) - case .top: - processSnap(snapped, self.topEdgeView) - self.edgePreviewUpdated(snapped) - case .left: - processSnap(snapped, self.leftEdgeView) - self.edgePreviewUpdated(snapped) - case .right: - processSnap(snapped, self.rightEdgeView) - self.edgePreviewUpdated(snapped) - case .bottom: - processSnap(snapped, self.bottomEdgeView) - self.edgePreviewUpdated(snapped) - case let .rotation(angle): - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - if let angle, let view { - self.layer.insertSublayer(self.angleLayer, below: view.layer) - self.angleLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) - if self.angleLayer.opacity < 1.0 { - self.hapticFeedback.impact(.light) - } - transition.updateAlpha(layer: self.angleLayer, alpha: 1.0) - } else { - transition.updateAlpha(layer: self.angleLayer, alpha: 0.0) - } - } - } - view.onPositionUpdated = { [weak self] position in - if let self { - self.angleLayer.position = position - } - } - view.onInteractionUpdated = { [weak self] interacting in - if let self { - self.onInteractionUpdated(interacting) - } - } - +// let processSnap: (Bool, UIView) -> Void = { [weak self] snapped, snapView in +// guard let self else { +// return +// } +// let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) +// if snapped { +// self.insertSubview(snapView, belowSubview: view) +// if snapView.alpha < 1.0 { +// self.hapticFeedback.impact(.light) +// } +// transition.updateAlpha(layer: snapView.layer, alpha: 1.0) +// } else { +// transition.updateAlpha(layer: snapView.layer, alpha: 0.0) +// } +// } +// +// view.onSnapUpdated = { [weak self, weak view] type, snapped in +// guard let self else { +// return +// } +// switch type { +// case .centerX: +// processSnap(snapped, self.xAxisView) +// case .centerY: +// processSnap(snapped, self.yAxisView) +// case .top: +// processSnap(snapped, self.topEdgeView) +// self.edgePreviewUpdated(snapped) +// case .left: +// processSnap(snapped, self.leftEdgeView) +// self.edgePreviewUpdated(snapped) +// case .right: +// processSnap(snapped, self.rightEdgeView) +// self.edgePreviewUpdated(snapped) +// case .bottom: +// processSnap(snapped, self.bottomEdgeView) +// self.edgePreviewUpdated(snapped) +// case let .rotation(angle): +// let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) +// if let angle, let view { +// self.layer.insertSublayer(self.angleLayer, below: view.layer) +// self.angleLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) +// if self.angleLayer.opacity < 1.0 { +// self.hapticFeedback.impact(.light) +// } +// transition.updateAlpha(layer: self.angleLayer, alpha: 1.0) +// } else { +// transition.updateAlpha(layer: self.angleLayer, alpha: 0.0) +// } +// } +// } +// view.onPositionUpdated = { [weak self] position in +// if let self { +// self.angleLayer.position = position +// } +// } +// view.onInteractionUpdated = { [weak self] interacting in +// if let self { +// self.onInteractionUpdated(interacting) +// } +// } +// view.update() self.addSubview(view) @@ -405,6 +405,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return view } + public func invalidate() { + for case let view as DrawingEntityView in self.subviews { + view.invalidate() + } + } + func duplicate(_ entity: DrawingEntity) -> DrawingEntity { let newEntity = entity.duplicate() self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) @@ -714,6 +720,14 @@ public class DrawingEntityView: UIView { } + func invalidate() { + self.selectionView = nil + self.containerView = nil + self.onSnapUpdated = { _, _ in } + self.onPositionUpdated = { _ in } + self.onInteractionUpdated = { _ in } + } + public func update(animated: Bool = false) { self.updateSelectionView() } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index ab70fb46d1..4baa1f3bd7 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -286,6 +286,10 @@ final class MediaPickerGridItemNode: GridItemNode { if self.backgroundNode.supernode == nil { self.insertSubnode(self.backgroundNode, at: 0) } + } else { + if self.draftNode.supernode != nil { + self.draftNode.removeFromSupernode() + } } if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index { @@ -315,6 +319,10 @@ final class MediaPickerGridItemNode: GridItemNode { if self.backgroundNode.supernode == nil { self.insertSubnode(self.backgroundNode, at: 0) } + } else { + if self.draftNode.supernode != nil { + self.draftNode.removeFromSupernode() + } } if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index b69eb964fc..1c6a1a4fdd 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -38,25 +38,30 @@ private struct CameraState { let flashModeDidChange: Bool let recording: Recording let duration: Double + let isDualCamEnabled: Bool func updatedMode(_ mode: CameraMode) -> CameraState { - return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) + return CameraState(mode: mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCamEnabled: self.isDualCamEnabled) } func updatedPosition(_ position: Camera.Position) -> CameraState { - return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) + return CameraState(mode: self.mode, position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCamEnabled: self.isDualCamEnabled) } func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration) + return CameraState(mode: self.mode, position: self.position, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration, isDualCamEnabled: self.isDualCamEnabled) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration, isDualCamEnabled: self.isDualCamEnabled) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration) + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration, isDualCamEnabled: self.isDualCamEnabled) + } + + func updatedIsDualCamEnabled(_ isDualCamEnabled: Bool) -> CameraState { + return CameraState(mode: self.mode, position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration, isDualCamEnabled: isDualCamEnabled) } } @@ -72,13 +77,14 @@ private let zoomControlTag = GenericComponentViewTag() private let captureControlsTag = GenericComponentViewTag() private let modeControlTag = GenericComponentViewTag() private let galleryButtonTag = GenericComponentViewTag() +private let dualButtonTag = GenericComponentViewTag() private final class CameraScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let camera: Camera - let changeMode: ActionSlot + let updateState: ActionSlot let hasAppeared: Bool let present: (ViewController) -> Void let push: (ViewController) -> Void @@ -87,7 +93,7 @@ private final class CameraScreenComponent: CombinedComponent { init( context: AccountContext, camera: Camera, - changeMode: ActionSlot, + updateState: ActionSlot, hasAppeared: Bool, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, @@ -95,7 +101,7 @@ private final class CameraScreenComponent: CombinedComponent { ) { self.context = context self.camera = camera - self.changeMode = changeMode + self.updateState = updateState self.hasAppeared = hasAppeared self.present = present self.push = push @@ -138,6 +144,7 @@ private final class CameraScreenComponent: CombinedComponent { fileprivate let camera: Camera private let present: (ViewController) -> Void private let completion: ActionSlot> + private let updateState: ActionSlot private var cameraStateDisposable: Disposable? private var resultDisposable = MetaDisposable() @@ -146,17 +153,22 @@ private final class CameraScreenComponent: CombinedComponent { fileprivate var lastGalleryAsset: PHAsset? private var lastGalleryAssetsDisposable: Disposable? - var cameraState = CameraState(mode: .photo, position: .unspecified, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0) + var cameraState = CameraState(mode: .photo, position: .unspecified, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0, isDualCamEnabled: false) { + didSet { + self.updateState.invoke(self.cameraState) + } + } var swipeHint: CaptureControlsComponent.SwipeHint = .none var isTransitioning = false private let hapticFeedback = HapticFeedback() - init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>) { + init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>, updateState: ActionSlot) { self.context = context self.camera = camera self.present = present self.completion = completion + self.updateState = updateState super.init() @@ -222,6 +234,13 @@ private final class CameraScreenComponent: CombinedComponent { self.hapticFeedback.impact(.light) } + func toggleDualCamera() { + let isEnabled = !self.cameraState.isDualCamEnabled + self.camera.setDualCamEnabled(isEnabled) + self.cameraState = self.cameraState.updatedIsDualCamEnabled(isEnabled) + self.updated(transition: .easeInOut(duration: 0.1)) + } + func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { guard hint != self.swipeHint else { return @@ -236,8 +255,8 @@ private final class CameraScreenComponent: CombinedComponent { switch value { case .began: return .single(.pendingImage) - case let .finished(image): - return .single(.image(image)) + case let .finished(mainImage, additionalImage, _): + return .single(.image(mainImage, additionalImage)) case .failed: return .complete() } @@ -288,7 +307,7 @@ private final class CameraScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion) + return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion, updateState: self.updateState) } static var body: Body { @@ -296,12 +315,16 @@ private final class CameraScreenComponent: CombinedComponent { let captureControls = Child(CaptureControlsComponent.self) let zoomControl = Child(ZoomComponent.self) let flashButton = Child(CameraButton.self) + let flipButton = Child(CameraButton.self) +// let dualButton = Child(CameraButton.self) let modeControl = Child(ModeComponent.self) let hintLabel = Child(HintLabelComponent.self) let timeBackground = Child(RoundedRectangle.self) let timeLabel = Child(MultilineTextComponent.self) + let flipAnimationAction = ActionSlot() + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component @@ -309,12 +332,14 @@ private final class CameraScreenComponent: CombinedComponent { let controller = environment.controller let availableSize = context.availableSize + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + let topControlInset: CGFloat = 20.0 - - component.changeMode.connect({ [weak state] mode in - state?.updateCameraMode(mode) - }) - if case .none = state.cameraState.recording, !state.isTransitioning { let cancelButton = cancelButton.update( component: CameraButton( @@ -341,7 +366,6 @@ private final class CameraScreenComponent: CombinedComponent { .position(CGPoint(x: topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) - .cornerRadius(20.0) ) let flashContentComponent: AnyComponentWithIdentity @@ -402,8 +426,33 @@ private final class CameraScreenComponent: CombinedComponent { .position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) - .cornerRadius(20.0) ) + +// if #available(iOS 13.0, *) { +// let dualButton = dualButton.update( +// component: CameraButton( +// content: AnyComponentWithIdentity( +// id: "dual", +// component: AnyComponent( +// DualIconComponent(isSelected: state.cameraState.isDualCamEnabled) +// ) +// ), +// action: { [weak state] in +// guard let state else { +// return +// } +// state.toggleDualCamera() +// } +// ).tagged(dualButtonTag), +// availableSize: CGSize(width: 40.0, height: 40.0), +// transition: .immediate +// ) +// context.add(dualButton +// .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0)) +// .appear(.default(scale: true)) +// .disappear(.default(scale: true)) +// ) +// } } if case .holding = state.cameraState.recording { @@ -447,6 +496,7 @@ private final class CameraScreenComponent: CombinedComponent { let captureControls = captureControls.update( component: CaptureControlsComponent( + isTablet: isTablet, shutterState: shutterState, lastGalleryAsset: state.lastGalleryAsset, tag: captureControlsTag, @@ -509,6 +559,34 @@ private final class CameraScreenComponent: CombinedComponent { .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)) ) + if isTablet { + let flipButton = flipButton.update( + component: CameraButton( + content: AnyComponentWithIdentity( + id: "flip", + component: AnyComponent( + FlipButtonContentComponent(action: flipAnimationAction) + ) + ), + minSize: CGSize(width: 44.0, height: 44.0), + action: { +// let currentTimestamp = CACurrentMediaTime() +// if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 { +// return +// } +// self.lastFlipTimestamp = currentTimestamp + state.togglePosition() + flipAnimationAction.invoke(Void()) + } + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(flipButton + .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)) + ) + } + var isVideoRecording = false if case .video = state.cameraState.mode { isVideoRecording = true @@ -658,7 +736,7 @@ public class CameraScreen: ViewController { public enum Result { case pendingImage - case image(UIImage) + case image(UIImage, UIImage?) case video(String, UIImage?, PixelDimensions) case asset(PHAsset) case draft(MediaEditorDraft) @@ -707,8 +785,10 @@ public class CameraScreen: ViewController { private let previewContainerView: UIView fileprivate let previewView: CameraPreviewView? fileprivate let simplePreviewView: CameraSimplePreviewView? + fileprivate var additionalPreviewView: CameraSimplePreviewView? fileprivate let previewBlurView: BlurView private var previewSnapshotView: UIView? + private var additionalPreviewSnapshotView: UIView? fileprivate let previewFrameLeftDimView: UIView fileprivate let previewFrameRightDimView: UIView fileprivate let transitionDimView: UIView @@ -719,8 +799,9 @@ public class CameraScreen: ViewController { private var validLayout: ContainerViewLayout? private var changingPositionDisposable: Disposable? - - private let changeMode = ActionSlot() + private var isDualCamEnabled = false + private var cameraPosition: Camera.Position = .back + private let completion = ActionSlot>() private var effectivePreviewView: UIView { @@ -733,6 +814,36 @@ public class CameraScreen: ViewController { } } + private var currentPreviewView: UIView { + if let simplePreviewView = self.simplePreviewView { + if let additionalPreviewView = self.additionalPreviewView { + if self.isDualCamEnabled && cameraPosition == .front { + return additionalPreviewView + } else { + return simplePreviewView + } + } else { + return simplePreviewView + } + } else if let previewView = self.previewView { + return previewView + } else { + fatalError() + } + } + + private var currentAdditionalPreviewView: UIView? { + if let additionalPreviewView = self.additionalPreviewView { + if self.isDualCamEnabled && cameraPosition == .front { + return self.simplePreviewView + } else { + return additionalPreviewView + } + } else { + return nil + } + } + fileprivate var previewBlurPromise = ValuePromise(false) init(controller: CameraScreen) { @@ -766,8 +877,11 @@ public class CameraScreen: ViewController { self.camera = holder.camera } else { if useSimplePreviewView { - self.simplePreviewView = CameraSimplePreviewView() + self.simplePreviewView = CameraSimplePreviewView(frame: .zero, additional: false) self.previewView = nil + + self.additionalPreviewView = CameraSimplePreviewView(frame: .zero, additional: true) + self.additionalPreviewView?.clipsToBounds = true } else { self.previewView = CameraPreviewView(test: false)! self.simplePreviewView = nil @@ -778,7 +892,8 @@ public class CameraScreen: ViewController { cameraFrontPosition = true } - self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: cameraFrontPosition ? .front : .back, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.simplePreviewView) + self.cameraPosition = cameraFrontPosition ? .front : .back + self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: self.cameraPosition, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.simplePreviewView, secondaryPreviewView: self.additionalPreviewView) if !useSimplePreviewView { #if targetEnvironment(simulator) #else @@ -816,28 +931,45 @@ public class CameraScreen: ViewController { self.containerView.addSubview(self.transitionDimView) self.view.addSubview(self.transitionCornersView) + if let additionalPreviewView = self.additionalPreviewView { + self.previewContainerView.insertSubview(additionalPreviewView, at: 1) + } + self.changingPositionDisposable = combineLatest( queue: Queue.mainQueue(), - self.camera.changingPosition, + self.camera.modeChange, self.previewBlurPromise.get() - ).start(next: { [weak self] changingPosition, forceBlur in + ).start(next: { [weak self] modeChange, forceBlur in if let self { - if changingPosition { + if modeChange != .none { if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { self.simplePreviewView?.addSubview(snapshot) self.previewSnapshotView = snapshot } - UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { - self.previewBlurView.effect = UIBlurEffect(style: .dark) - }) + if case .position = modeChange { + UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + }) + } else { + if let additionalPreviewView = self.additionalPreviewView { + self.previewContainerView.insertSubview(self.previewBlurView, belowSubview: additionalPreviewView) + } + UIView.animate(withDuration: 0.4) { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + } + } } else if forceBlur { UIView.animate(withDuration: 0.4) { self.previewBlurView.effect = UIBlurEffect(style: .dark) } } else { - UIView.animate(withDuration: 0.4) { + UIView.animate(withDuration: 0.4, animations: { self.previewBlurView.effect = nil - } + }, completion: { _ in + if let additionalPreviewView = self.additionalPreviewView { + self.previewContainerView.insertSubview(self.previewBlurView, aboveSubview: additionalPreviewView) + } + }) if let previewSnapshotView = self.previewSnapshotView { self.previewSnapshotView = nil @@ -847,6 +979,15 @@ public class CameraScreen: ViewController { previewSnapshotView.removeFromSuperview() }) } + + if let previewSnapshotView = self.additionalPreviewSnapshotView { + self.additionalPreviewSnapshotView = nil + UIView.animate(withDuration: 0.25, animations: { + previewSnapshotView.alpha = 0.0 + }, completion: { _ in + previewSnapshotView.removeFromSuperview() + }) + } } } }) @@ -864,6 +1005,8 @@ public class CameraScreen: ViewController { Queue.mainQueue().async { self.effectivePreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) self.simplePreviewView?.isEnabled = false + + self.additionalPreviewView?.isEnabled = false } } else { Queue.mainQueue().async { @@ -873,6 +1016,7 @@ public class CameraScreen: ViewController { } } self.simplePreviewView?.isEnabled = false + self.additionalPreviewView?.isEnabled = false self.camera.stopCapture() } } @@ -882,6 +1026,28 @@ public class CameraScreen: ViewController { ) } } + + self.updateState.connect { [weak self] state in + if let self { + let previousPosition = self.cameraPosition + self.cameraPosition = state.position + self.isDualCamEnabled = state.isDualCamEnabled + + if self.isDualCamEnabled && previousPosition != state.position, let additionalPreviewView = self.additionalPreviewView { + if state.position == .front { + additionalPreviewView.superview?.sendSubviewToBack(additionalPreviewView) + } else { + additionalPreviewView.superview?.insertSubview(additionalPreviewView, aboveSubview: self.simplePreviewView!) + } + CATransaction.begin() + CATransaction.setDisableActions(true) + self.requestUpdateLayout(hasAppeared: false, transition: .immediate) + CATransaction.commit() + } else { + self.requestUpdateLayout(hasAppeared: false, transition: .spring(duration: 0.4)) + } + } + } } deinit { @@ -936,7 +1102,7 @@ public class CameraScreen: ViewController { self.isDismissing = true 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 { + } else if translation.y < -10.0 && abs(translation.y) > abs(translation.x) { controller.presentGallery(fromGesture: true) gestureRecognizer.isEnabled = false gestureRecognizer.isEnabled = true @@ -1031,6 +1197,10 @@ public class CameraScreen: ViewController { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) transition.setAlpha(view: view, alpha: 0.0) } + if let view = self.componentHost.findTaggedView(tag: dualButtonTag) { + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + transition.setAlpha(view: view, alpha: 0.0) + } if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) transition.setAlpha(view: view, alpha: 0.0) @@ -1048,6 +1218,7 @@ public class CameraScreen: ViewController { func pauseCameraCapture() { self.simplePreviewView?.isEnabled = false + self.additionalPreviewView?.isEnabled = false Queue.mainQueue().after(0.3) { self.previewBlurPromise.set(true) } @@ -1060,7 +1231,12 @@ public class CameraScreen: ViewController { self.simplePreviewView?.addSubview(snapshot) self.previewSnapshotView = snapshot } + if let snapshot = self.additionalPreviewView?.snapshotView(afterScreenUpdates: false) { + self.additionalPreviewView?.addSubview(snapshot) + self.additionalPreviewSnapshotView = snapshot + } self.simplePreviewView?.isEnabled = true + self.additionalPreviewView?.isEnabled = true self.camera.startCapture() if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { @@ -1090,6 +1266,10 @@ public class CameraScreen: ViewController { 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: dualButtonTag) { + 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) @@ -1138,6 +1318,21 @@ public class CameraScreen: ViewController { }) self.controller?.present(controller, in: .current) } + + func presentDualCameraTooltip() { + guard let sourceView = self.componentHost.findTaggedView(tag: dualButtonTag) else { + return + } + + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "Enable Dual Camera Mode", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + return .ignore + }) + self.controller?.present(tooltipController, in: .current) + } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) @@ -1212,6 +1407,8 @@ public class CameraScreen: ViewController { } else if hasAppeared && !self.hasAppeared { self.hasAppeared = hasAppeared transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) + + self.presentDualCameraTooltip() } let componentSize = self.componentHost.update( @@ -1220,7 +1417,7 @@ public class CameraScreen: ViewController { CameraScreenComponent( context: self.context, camera: self.camera, - changeMode: self.changeMode, + updateState: self.updateState, hasAppeared: self.hasAppeared, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) @@ -1266,8 +1463,20 @@ public class CameraScreen: ViewController { } transition.setFrame(view: self.previewContainerView, frame: previewFrame) - transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) + self.currentPreviewView.layer.cornerRadius = 0.0 + transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) + + + if let additionalPreviewView = self.currentAdditionalPreviewView { + additionalPreviewView.layer.cornerRadius = 80.0 + let additionalPreviewFrame = CGRect(origin: CGPoint(x: previewFrame.width - 160.0 - 10.0 + (self.isDualCamEnabled ? 0.0 : 180.0), y: previewFrame.height - 160.0 - 81.0), size: CGSize(width: 160.0, height: 160.0)) + transition.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center) + transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size)) + + transition.setScale(view: additionalPreviewView, scale: self.isDualCamEnabled ? 1.0 : 0.1) + transition.setAlpha(view: additionalPreviewView, alpha: self.isDualCamEnabled ? 1.0 : 0.0) + } self.previewFrameLeftDimView.isHidden = !isTablet transition.setFrame(view: self.previewFrameLeftDimView, frame: CGRect(origin: .zero, size: CGSize(width: viewfinderFrame.minX, height: viewfinderFrame.height))) @@ -1386,6 +1595,8 @@ public class CameraScreen: ViewController { self.hapticFeedback.impact(.light) } + self.dismissAllTooltips() + var didStopCameraCapture = false let stopCameraCapture = { [weak self] in guard !didStopCameraCapture, let self else { @@ -1548,3 +1759,86 @@ public class CameraScreen: ViewController { } } } + +private final class DualIconComponent: Component { + typealias EnvironmentType = Empty + + let isSelected: Bool + + init( + isSelected: Bool + ) { + self.isSelected = isSelected + } + + static func ==(lhs: DualIconComponent, rhs: DualIconComponent) -> Bool { + if lhs.isSelected != rhs.isSelected { + return false + } + return true + } + + final class View: UIView { + private let iconView = UIImageView() + + private var component: DualIconComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + let image = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0)), size: image.size)) + } + }) + + let selectedImage = generateImage(CGSize(width: 36.0, height: 36.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size)) + + if let image = UIImage(bundleImageName: "Camera/DualIcon"), let cgImage = image.cgImage { + context.setBlendMode(.clear) + context.clip(to: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) / 2.0), y: floorToScreenPixels((size.height - image.size.height) / 2.0)), size: image.size), mask: cgImage) + context.fill(CGRect(origin: .zero, size: size)) + } + }) + + self.iconView.image = image + self.iconView.highlightedImage = selectedImage + + self.iconView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + self.iconView.layer.shadowRadius = 4.0 + self.iconView.layer.shadowColor = UIColor.black.cgColor + self.iconView.layer.shadowOpacity = 0.2 + + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DualIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: 36.0, height: 36.0) + self.iconView.frame = CGRect(origin: .zero, size: size) + self.iconView.isHighlighted = component.isSelected + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index fbc9b4f74e..e2efca1bfa 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -200,7 +200,7 @@ private final class ShutterButtonContentComponent: Component { } } -private final class FlipButtonContentComponent: Component { +final class FlipButtonContentComponent: Component { private let action: ActionSlot init(action: ActionSlot) { @@ -277,6 +277,7 @@ final class CaptureControlsComponent: Component { case flip } + let isTablet: Bool let shutterState: ShutterButtonState let lastGalleryAsset: PHAsset? let tag: AnyObject? @@ -291,6 +292,7 @@ final class CaptureControlsComponent: Component { let zoomUpdated: (CGFloat) -> Void init( + isTablet: Bool, shutterState: ShutterButtonState, lastGalleryAsset: PHAsset?, tag: AnyObject?, @@ -304,6 +306,7 @@ final class CaptureControlsComponent: Component { swipeHintUpdated: @escaping (SwipeHint) -> Void, zoomUpdated: @escaping (CGFloat) -> Void ) { + self.isTablet = isTablet self.shutterState = shutterState self.lastGalleryAsset = lastGalleryAsset self.tag = tag @@ -319,6 +322,9 @@ final class CaptureControlsComponent: Component { } static func ==(lhs: CaptureControlsComponent, rhs: CaptureControlsComponent) -> Bool { + if lhs.isTablet != rhs.isTablet { + return false + } if lhs.shutterState != rhs.shutterState { return false } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal index 42eebfdf16..fd71d03e68 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal @@ -22,6 +22,7 @@ static inline float4 sRGBGammaDecode(const float4 rgba) { result.g = sRGBnonLinearNormToLinear(rgba.g); result.b = sRGBnonLinearNormToLinear(rgba.b); return rgba; +} static inline float4 BT709Decode(const float Y, const float Cb, const float Cr) { float Yn = Y; @@ -57,6 +58,6 @@ fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]], float4 pixel = BT709Decode(Y, Cb, Cr); pixel = sRGBGammaDecode(pixel); - pixel.rgb = pow(pixel.rgb, 1.0 / 2.2); + //pixel.rgb = pow(pixel.rgb, 1.0 / 2.2); return pixel; } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index eb1601da83..1d87888c6d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -261,10 +261,14 @@ final class MediaEditorScreenComponent: Component { var delay: Double = 0.0 for button in buttons { if let view = button.view { - view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: delay) - delay += 0.05 + view.alpha = 0.0 + Queue.mainQueue().after(delay, { + view.alpha = 1.0 + view.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + }) + delay += 0.03 } } @@ -928,8 +932,13 @@ final class MediaEditorScreenComponent: Component { ), action: { [weak self, weak state] in if let self, let mediaEditor = self.component?.mediaEditor { - mediaEditor.setVideoIsMuted(!mediaEditor.values.videoIsMuted) + let isMuted = !mediaEditor.values.videoIsMuted + mediaEditor.setVideoIsMuted(isMuted) state?.updated() + + if let controller = environment.controller() as? MediaEditorScreen { + controller.node.presentMutedTooltip() + } } } )), @@ -1180,7 +1189,7 @@ public final class MediaEditorScreen: ViewController { private var isDisplayingTool = false private var isInteractingWithEntities = false - private var isEnhacing = false + private var isEnhancing = false private var isDismissing = false private var dismissOffset: CGFloat = 0.0 private var isDismissed = false @@ -1362,6 +1371,25 @@ public final class MediaEditorScreen: ViewController { } self.entitiesView.add(mediaEntity, announce: false) + if case let .image(_, _, additionalImage) = subject, let additionalImage { + let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + context.addEllipse(in: bounds) + context.clip() + + if let cgImage = additionalImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: (size.width - additionalImage.size.width) / 2.0, y: (size.height - additionalImage.size.height) / 2.0), size: additionalImage.size)) + } + }) + let imageEntity = DrawingStickerEntity(content: .image(image ?? additionalImage)) + imageEntity.referenceDrawingSize = storyDimensions + imageEntity.scale = 1.49 + imageEntity.mirrored = true + imageEntity.position = CGPoint(x: storyDimensions.width - 224.0, y: storyDimensions.height - 403.0) + self.entitiesView.add(imageEntity, announce: false) + } + let initialPosition = mediaEntity.position let initialScale = mediaEntity.scale let initialRotation = mediaEntity.rotation @@ -1548,20 +1576,20 @@ public final class MediaEditorScreen: ViewController { let velocity = gestureRecognizer.velocity(in: self.view) switch gestureRecognizer.state { case .changed: - if abs(translation.y) > 10.0 && !self.isEnhacing && hasSwipeToDismiss { + if abs(translation.y) > 10.0 && !self.isEnhancing && hasSwipeToDismiss { if !self.isDismissing { self.isDismissing = true controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } } else if abs(translation.x) > 10.0 && !self.isDismissing { - self.isEnhacing = true + self.isEnhancing = true controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } if self.isDismissing { self.dismissOffset = translation.y controller.requestLayout(transition: .immediate) - } else if self.isEnhacing { + } else if self.isEnhancing { if let mediaEditor = self.mediaEditor { let value = mediaEditor.getToolValue(.enhance) as? Float ?? 0.0 let delta = Float((translation.x / self.frame.width) * 1.5) @@ -1581,8 +1609,10 @@ public final class MediaEditorScreen: ViewController { controller.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } else { - self.isEnhacing = false - controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + self.isEnhancing = false + Queue.mainQueue().after(0.5) { + controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + } } default: break @@ -1861,7 +1891,24 @@ public final class MediaEditorScreen: ViewController { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story.", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + return .ignore + }) + self.controller?.present(tooltipController, in: .current) + } + + func presentMutedTooltip() { + guard let sourceView = self.componentHost.findTaggedView(tag: muteButtonTag) else { + return + } + + let isMuted = self.mediaEditor?.values.videoIsMuted ?? false + + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: isMuted ? "The story will have no sound." : "The story will have sound." , location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) self.controller?.present(tooltipController, in: .current) @@ -2187,7 +2234,7 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.addSubview(toolValueView) } transition.setFrame(view: toolValueView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - toolValueSize.width) / 2.0), y: 88.0), size: toolValueSize)) - transition.setAlpha(view: toolValueView, alpha: self.isEnhacing ? 1.0 : 0.0) + transition.setAlpha(view: toolValueView, alpha: self.isEnhancing ? 1.0 : 0.0) } transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) @@ -2225,14 +2272,14 @@ public final class MediaEditorScreen: ViewController { } public enum Subject { - case image(UIImage, PixelDimensions) + case image(UIImage, PixelDimensions, UIImage?) case video(String, UIImage?, PixelDimensions) case asset(PHAsset) case draft(MediaEditorDraft, Int64?) var dimensions: PixelDimensions { switch self { - case let .image(_, dimensions), let .video(_, _, dimensions): + case let .image(_, dimensions, _), let .video(_, _, dimensions): return dimensions case let .asset(asset): return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) @@ -2243,7 +2290,7 @@ public final class MediaEditorScreen: ViewController { var editorSubject: MediaEditor.Subject { switch self { - case let .image(image, dimensions): + case let .image(image, dimensions, _): return .image(image, dimensions) case let .video(videoPath, transitionImage, dimensions): return .video(videoPath, transitionImage, dimensions) @@ -2256,7 +2303,7 @@ public final class MediaEditorScreen: ViewController { var mediaContent: DrawingMediaEntity.Content { switch self { - case let .image(image, dimensions): + case let .image(image, dimensions, _): return .image(image, dimensions) case let .video(videoPath, _, dimensions): return .video(videoPath, dimensions) @@ -2618,7 +2665,7 @@ public final class MediaEditorScreen: ViewController { if saveDraft { self.saveDraft(id: nil) } else { -// if case let .draft(draft) = self.node.subject { +// if case let .draft(draft, _) = self.node.subject { // removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) // } } @@ -2626,6 +2673,7 @@ public final class MediaEditorScreen: ViewController { if let mediaEditor = self.node.mediaEditor { mediaEditor.invalidate() } + self.node.entitiesView.invalidate() self.cancelled(saveDraft) @@ -2680,7 +2728,7 @@ public final class MediaEditorScreen: ViewController { } switch subject { - case let .image(image, dimensions): + case let .image(image, dimensions, _): saveImageDraft(image, dimensions) case let .video(path, _, dimensions): saveVideoDraft(path, dimensions) @@ -2726,6 +2774,7 @@ public final class MediaEditorScreen: ViewController { self.dismissAllTooltips() mediaEditor.invalidate() + self.node.entitiesView.invalidate() if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) @@ -2746,7 +2795,7 @@ public final class MediaEditorScreen: ViewController { let videoResult: Result.VideoResult let duration: Double switch subject { - case let .image(image, _): + case let .image(image, _, _): let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" if let data = image.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: tempImagePath)) @@ -2872,7 +2921,7 @@ public final class MediaEditorScreen: ViewController { case let .video(path, _, _): let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) exportSubject = .single(.video(asset)) - case let .image(image, _): + case let .image(image, _, _): exportSubject = .single(.image(image)) case let .asset(asset): exportSubject = Signal { subscriber in diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 7f6ba785ef..fad28704d8 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1289,7 +1289,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> 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] = [] diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index d1ed2e215b..3ec6b643ea 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -19,6 +19,7 @@ import Postbox import AvatarNode import MediaEditorScreen import ImageCompression +import ShareWithPeersScreen public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -239,6 +240,7 @@ public final class StoryItemSetContainerComponent: Component { weak var actionSheet: ActionSheetController? weak var contextController: ContextController? + weak var privacyController: ShareWithPeersScreen? var component: StoryItemSetContainerComponent? weak var state: EmptyComponentState? @@ -489,6 +491,12 @@ public final class StoryItemSetContainerComponent: Component { if self.inputPanelExternalState.isEditing || component.isProgressPaused || self.actionSheet != nil || self.contextController != nil || self.sendMessageContext.audioRecorderValue != nil || self.sendMessageContext.videoRecorderValue != nil || self.displayViewList { return true } + if self.privacyController != nil { + return true + } + if self.isEditingStory { + return true + } if self.sendMessageContext.attachmentController != nil { return true } @@ -2055,12 +2063,41 @@ public final class StoryItemSetContainerComponent: Component { } component.externalState.derivedMediaSize = contentFrame.size - component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY) + if focusedItem?.isMy == true { + component.externalState.derivedBottomInset = availableSize.height - contentFrame.maxY + } else { + component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY) + } return contentSize } private func openItemPrivacySettings() { + guard let context = self.component?.context, let privacy = self.component?.slice.item.storyItem.privacy else { + return + } + + let stateContext = ShareWithPeersScreen.StateContext(context: context, subject: .stories) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + let controller = ShareWithPeersScreen( + context: context, + initialPrivacy: privacy, + stateContext: stateContext, + completion: { [weak self] privacy in + self?.updateIsProgressPaused() + }, + editCategory: { privacy in + + } + ) + self.component?.controller()?.push(controller) + + self.privacyController = controller + self.updateIsProgressPaused() + }) } private func openStoryEditing() { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 2b13335b21..3c3b9830d0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -708,7 +708,7 @@ public final class StoryPeerListItemComponent: Component { } let titleSize = self.title.update( - transition: .immediate, + transition: titleTransition, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: titleString, font: Font.regular(11.0), textColor: component.theme.list.itemPrimaryTextColor)), maximumNumberOfLines: 1 @@ -725,8 +725,8 @@ public final class StoryPeerListItemComponent: Component { } titleTransition.setPosition(view: titleView, position: titleFrame.center) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - transition.setScale(view: titleView, scale: effectiveScale) - transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction) + titleTransition.setScale(view: titleView, scale: effectiveScale) + titleTransition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction) } if let ringAnimation = component.ringAnimation { diff --git a/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/Contents.json new file mode 100644 index 0000000000..23023bf05e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "VideoMessagePlaceholder.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/VideoMessagePlaceholder.jpg b/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/VideoMessagePlaceholder.jpg new file mode 100644 index 0000000000..5de9839e6f Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Camera/SelfiePlaceholder.imageset/VideoMessagePlaceholder.jpg differ diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 6284bc00eb..c2a37e3a55 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -290,8 +290,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon switch value { case .pendingImage: return nil - case let .image(image): - return .image(image, PixelDimensions(image.size)) + case let .image(image, additionalImage): + return .image(image, PixelDimensions(image.size), additionalImage) case let .video(path, transitionImage, dimensions): return .video(path, transitionImage, dimensions) case let .asset(asset):