Merge commit 'b9dfb3d2a1a75d56f7a4e14c2109a821accbe677'

This commit is contained in:
Ali 2023-07-07 02:03:56 +04:00
commit 2657296945
31 changed files with 770 additions and 371 deletions

View File

@ -8,13 +8,17 @@ final class CameraSession {
private let singleSession: AVCaptureSession?
private let multiSession: Any?
let hasMultiCam: Bool
init() {
if #available(iOS 13.0, *), AVCaptureMultiCamSession.isMultiCamSupported {
self.multiSession = AVCaptureMultiCamSession()
self.singleSession = nil
self.hasMultiCam = true
} else {
self.singleSession = AVCaptureSession()
self.multiSession = nil
self.hasMultiCam = false
}
}
@ -119,23 +123,9 @@ private final class CameraContext {
}
}
var previewView: CameraPreviewView? {
didSet {
}
}
var simplePreviewView: CameraSimplePreviewView? {
didSet {
if let oldValue {
Queue.mainQueue().async {
oldValue.invalidate()
self.simplePreviewView?.setSession(self.session.session, autoConnect: true)
}
}
}
}
var previewView: CameraPreviewView?
var simplePreviewView: CameraSimplePreviewView?
var secondaryPreviewView: CameraSimplePreviewView?
private var lastSnapshotTimestamp: Double = CACurrentMediaTime()
@ -161,7 +151,6 @@ private final class CameraContext {
}
}
private var videoOrientation: AVCaptureVideoOrientation?
init(queue: Queue, session: CameraSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?, secondaryPreviewView: CameraSimplePreviewView?) {
self.queue = queue
self.session = session
@ -371,7 +360,7 @@ private final class CameraContext {
self.previewNode?.enqueue(sampleBuffer)
let timestamp = CACurrentMediaTime()
if timestamp > self.lastSnapshotTimestamp + 2.5 {
if timestamp > self.lastSnapshotTimestamp + 2.5, !self.mainDeviceContext.output.isRecording {
var mirror = false
if #available(iOS 13.0, *) {
mirror = connection.inputPorts.first?.sourceDevicePosition == .front
@ -450,7 +439,7 @@ private final class CameraContext {
}
func takePhoto() -> Signal<PhotoCaptureResult, NoError> {
let orientation = self.videoOrientation ?? .portrait
let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
if let additionalDeviceContext = self.additionalDeviceContext {
let dualPosition = self.positionValue
return combineLatest(
@ -565,9 +554,9 @@ public final class Camera {
session.session.automaticallyConfiguresApplicationAudioSession = false
session.session.automaticallyConfiguresCaptureDeviceForWideColor = false
if let previewView {
previewView.setSession(session.session, autoConnect: false)
previewView.setSession(session.session, autoConnect: !session.hasMultiCam)
}
if let secondaryPreviewView {
if let secondaryPreviewView, session.hasMultiCam {
secondaryPreviewView.setSession(session.session, autoConnect: false)
}

View File

@ -27,7 +27,11 @@ class CameraInput {
if let videoInput = try? AVCaptureDeviceInput(device: device) {
self.videoInput = videoInput
if session.session.canAddInput(videoInput) {
session.session.addInputWithNoConnections(videoInput)
if session.hasMultiCam {
session.session.addInputWithNoConnections(videoInput)
} else {
session.session.addInput(videoInput)
}
}
}
}

View File

@ -118,7 +118,11 @@ final class CameraOutput: NSObject {
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)
if session.hasMultiCam {
session.session.addOutputWithNoConnections(self.videoOutput)
} else {
session.session.addOutput(self.videoOutput)
}
self.videoOutput.setSampleBufferDelegate(self, queue: self.queue)
}
if audio, session.session.canAddOutput(self.audioOutput) {
@ -126,7 +130,11 @@ final class CameraOutput: NSObject {
self.audioOutput.setSampleBufferDelegate(self, queue: self.queue)
}
if photo, session.session.canAddOutput(self.photoOutput) {
session.session.addOutputWithNoConnections(self.photoOutput)
if session.hasMultiCam {
session.session.addOutputWithNoConnections(self.photoOutput)
} else {
session.session.addOutput(self.photoOutput)
}
}
if metadata, session.session.canAddOutput(self.metadataOutput) {
session.session.addOutput(self.metadataOutput)
@ -137,7 +145,7 @@ final class CameraOutput: NSObject {
}
}
if #available(iOS 13.0, *) {
if #available(iOS 13.0, *), session.hasMultiCam {
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)
@ -163,57 +171,7 @@ final class CameraOutput: NSObject {
}
}
}
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 {
@ -314,6 +272,10 @@ final class CameraOutput: NSObject {
}
}
var isRecording: Bool {
return self.videoRecorder != nil
}
private var recordingCompletionPipe = ValuePipe<VideoCaptureResult>()
func startRecording(isDualCamera: Bool, position: Camera.Position? = nil) -> Signal<Double, NoError> {
guard self.videoRecorder == nil else {

View File

@ -32,8 +32,7 @@ public class CameraSimplePreviewView: UIView {
} else {
statusBarOrientation = UIApplication.shared.statusBarOrientation
}
let videoOrientation: AVCaptureVideoOrientation = statusBarOrientation.videoOrientation
// videoPreviewLayer.frame = view.layer.bounds
let videoOrientation = statusBarOrientation.videoOrientation
self.videoPreviewLayer.connection?.videoOrientation = videoOrientation
self.videoPreviewLayer.removeAllAnimations()
}
@ -78,7 +77,7 @@ public class CameraSimplePreviewView: UIView {
self.videoPreviewLayer.videoGravity = main ? .resizeAspectFill : .resizeAspect
self.placeholderView.contentMode = .scaleAspectFill
self.placeholderView.contentMode = main ? .scaleAspectFill : .scaleAspectFit
self.addSubview(self.placeholderView)
if main {

View File

@ -201,93 +201,157 @@ func exifOrientationForDeviceOrientation(_ deviceOrientation: UIDeviceOrientatio
}
}
/**
First crops the pixel buffer, then resizes it.
This function requires the caller to pass in both the source and destination
pixel buffers. The dimensions of destination pixel buffer should be at least
`scaleWidth` x `scaleHeight` pixels.
*/
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
to dstPixelBuffer: CVPixelBuffer,
cropX: Int,
cropY: Int,
cropWidth: Int,
cropHeight: Int,
scaleWidth: Int,
scaleHeight: Int) {
assert(CVPixelBufferGetWidth(dstPixelBuffer) >= scaleWidth)
assert(CVPixelBufferGetHeight(dstPixelBuffer) >= scaleHeight)
let srcFlags = CVPixelBufferLockFlags.readOnly
let dstFlags = CVPixelBufferLockFlags(rawValue: 0)
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(srcPixelBuffer, srcFlags) else {
print("Error: could not lock source pixel buffer")
return
}
defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, srcFlags) }
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(dstPixelBuffer, dstFlags) else {
print("Error: could not lock destination pixel buffer")
return
}
defer { CVPixelBufferUnlockBaseAddress(dstPixelBuffer, dstFlags) }
guard let srcData = CVPixelBufferGetBaseAddress(srcPixelBuffer),
let dstData = CVPixelBufferGetBaseAddress(dstPixelBuffer) else {
print("Error: could not get pixel buffer base address")
return
}
let srcBytesPerRow = CVPixelBufferGetBytesPerRow(srcPixelBuffer)
let offset = cropY*srcBytesPerRow + cropX*4
var srcBuffer = vImage_Buffer(data: srcData.advanced(by: offset),
height: vImagePixelCount(cropHeight),
width: vImagePixelCount(cropWidth),
rowBytes: srcBytesPerRow)
let dstBytesPerRow = CVPixelBufferGetBytesPerRow(dstPixelBuffer)
var dstBuffer = vImage_Buffer(data: dstData,
height: vImagePixelCount(scaleHeight),
width: vImagePixelCount(scaleWidth),
rowBytes: dstBytesPerRow)
let error = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(0))
if error != kvImageNoError {
print("Error:", error)
}
to dstPixelBuffer: CVPixelBuffer,
cropX: Int,
cropY: Int,
cropWidth: Int,
cropHeight: Int,
scaleWidth: Int,
scaleHeight: Int) {
assert(CVPixelBufferGetWidth(dstPixelBuffer) >= scaleWidth)
assert(CVPixelBufferGetHeight(dstPixelBuffer) >= scaleHeight)
let srcFlags = CVPixelBufferLockFlags.readOnly
let dstFlags = CVPixelBufferLockFlags(rawValue: 0)
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(srcPixelBuffer, srcFlags) else {
print("Error: could not lock source pixel buffer")
return
}
defer { CVPixelBufferUnlockBaseAddress(srcPixelBuffer, srcFlags) }
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(dstPixelBuffer, dstFlags) else {
print("Error: could not lock destination pixel buffer")
return
}
defer { CVPixelBufferUnlockBaseAddress(dstPixelBuffer, dstFlags) }
guard let srcData = CVPixelBufferGetBaseAddress(srcPixelBuffer),
let dstData = CVPixelBufferGetBaseAddress(dstPixelBuffer) else {
print("Error: could not get pixel buffer base address")
return
}
let srcBytesPerRow = CVPixelBufferGetBytesPerRow(srcPixelBuffer)
let offset = cropY*srcBytesPerRow + cropX*4
var srcBuffer = vImage_Buffer(data: srcData.advanced(by: offset),
height: vImagePixelCount(cropHeight),
width: vImagePixelCount(cropWidth),
rowBytes: srcBytesPerRow)
let dstBytesPerRow = CVPixelBufferGetBytesPerRow(dstPixelBuffer)
var dstBuffer = vImage_Buffer(data: dstData,
height: vImagePixelCount(scaleHeight),
width: vImagePixelCount(scaleWidth),
rowBytes: dstBytesPerRow)
let error = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(0))
if error != kvImageNoError {
print("Error:", error)
}
}
/**
Resizes a CVPixelBuffer to a new width and height.
This function requires the caller to pass in both the source and destination
pixel buffers. The dimensions of destination pixel buffer should be at least
`width` x `height` pixels.
*/
func resizePixelBuffer(from srcPixelBuffer: CVPixelBuffer,
to dstPixelBuffer: CVPixelBuffer,
width: Int, height: Int) {
resizePixelBuffer(from: srcPixelBuffer, to: dstPixelBuffer,
cropX: 0, cropY: 0,
cropWidth: CVPixelBufferGetWidth(srcPixelBuffer),
cropHeight: CVPixelBufferGetHeight(srcPixelBuffer),
scaleWidth: width, scaleHeight: height)
to dstPixelBuffer: CVPixelBuffer,
width: Int, height: Int) {
resizePixelBuffer(from: srcPixelBuffer, to: dstPixelBuffer,
cropX: 0, cropY: 0,
cropWidth: CVPixelBufferGetWidth(srcPixelBuffer),
cropHeight: CVPixelBufferGetHeight(srcPixelBuffer),
scaleWidth: width, scaleHeight: height)
}
/**
Resizes a CVPixelBuffer to a new width and height, using Core Image.
*/
func resizePixelBuffer(_ pixelBuffer: CVPixelBuffer,
width: Int, height: Int,
output: CVPixelBuffer, context: CIContext) {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let sx = CGFloat(width) / CGFloat(CVPixelBufferGetWidth(pixelBuffer))
let sy = CGFloat(height) / CGFloat(CVPixelBufferGetHeight(pixelBuffer))
let scaleTransform = CGAffineTransform(scaleX: sx, y: sy)
let scaledImage = ciImage.transformed(by: scaleTransform)
context.render(scaledImage, to: output)
width: Int, height: Int,
output: CVPixelBuffer, context: CIContext) {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let sx = CGFloat(width) / CGFloat(CVPixelBufferGetWidth(pixelBuffer))
let sy = CGFloat(height) / CGFloat(CVPixelBufferGetHeight(pixelBuffer))
let scaleTransform = CGAffineTransform(scaleX: sx, y: sy)
let scaledImage = ciImage.transformed(by: scaleTransform)
context.render(scaledImage, to: output)
}
func imageFromCVPixelBuffer(_ pixelBuffer: CVPixelBuffer, orientation: UIImage.Orientation) -> UIImage? {
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(
data: baseAddress,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
) else {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return nil
}
guard let cgImage = context.makeImage() else {
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return nil
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
}
extension CVPixelBuffer {
func deepCopy() -> CVPixelBuffer? {
let width = CVPixelBufferGetWidth(self)
let height = CVPixelBufferGetHeight(self)
let format = CVPixelBufferGetPixelFormatType(self)
let attributes: [NSObject: AnyObject] = [
kCVPixelBufferCGImageCompatibilityKey: true as AnyObject,
kCVPixelBufferCGBitmapContextCompatibilityKey: true as AnyObject
]
var newPixelBuffer: CVPixelBuffer?
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
width,
height,
format,
attributes as CFDictionary,
&newPixelBuffer
)
guard status == kCVReturnSuccess, let unwrappedPixelBuffer = newPixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(self, .readOnly)
CVPixelBufferLockBaseAddress(unwrappedPixelBuffer, [])
guard let sourceBaseAddress = CVPixelBufferGetBaseAddress(self),
let destinationBaseAddress = CVPixelBufferGetBaseAddress(unwrappedPixelBuffer) else {
CVPixelBufferUnlockBaseAddress(self, .readOnly)
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
return nil
}
let sourceBytesPerRow = CVPixelBufferGetBytesPerRow(self)
let destinationBytesPerRow = CVPixelBufferGetBytesPerRow(unwrappedPixelBuffer)
let imageSize = height * min(sourceBytesPerRow, destinationBytesPerRow)
memcpy(destinationBaseAddress, sourceBaseAddress, imageSize)
CVPixelBufferUnlockBaseAddress(self, .readOnly)
CVPixelBufferUnlockBaseAddress(unwrappedPixelBuffer, [])
return unwrappedPixelBuffer
}
}

View File

@ -34,7 +34,7 @@ private final class VideoRecorderImpl {
private var videoInput: AVAssetWriterInput?
private var audioInput: AVAssetWriterInput?
private let imageContext: CIContext
private let imageContext = CIContext()
private var transitionImage: UIImage?
private var savedTransitionImage = false
@ -66,7 +66,6 @@ private final class VideoRecorderImpl {
self.configuration = configuration
self.videoTransform = videoTransform
self.url = fileUrl
self.imageContext = CIContext()
try? FileManager.default.removeItem(at: url)
guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else {
@ -162,10 +161,16 @@ private final class VideoRecorderImpl {
}
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
}
if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
self.savedTransitionImage = true
Queue.concurrentDefaultQueue().async {
Queue.concurrentBackgroundQueue().async {
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) {
self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
@ -175,14 +180,6 @@ private final class VideoRecorderImpl {
}
}
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)
}

View File

@ -157,6 +157,8 @@ public final class Button: Component {
super.init(frame: frame)
self.isExclusiveTouch = true
self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)

View File

@ -297,9 +297,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
if let shape = entity as? DrawingSimpleShapeEntity {
shape.position = center
shape.rotation = rotation
if setup {
shape.rotation = rotation
let size = self.newEntitySize()
shape.referenceDrawingSize = self.size
if shape.shapeType == .star {
@ -319,15 +318,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
} else if let sticker = entity as? DrawingStickerEntity {
sticker.position = center
sticker.rotation = rotation
if setup {
sticker.rotation = rotation
sticker.referenceDrawingSize = self.size
sticker.scale = zoomScale
}
} else if let bubble = entity as? DrawingBubbleEntity {
bubble.position = center
bubble.rotation = rotation
if setup {
bubble.rotation = rotation
let size = self.newEntitySize()
bubble.referenceDrawingSize = self.size
bubble.size = CGSize(width: size.width, height: round(size.height * 0.7))
@ -335,8 +334,8 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
}
} else if let text = entity as? DrawingTextEntity {
text.position = center
text.rotation = rotation
if setup {
text.rotation = rotation
text.referenceDrawingSize = self.size
text.width = floor(self.size.width * 0.9)
text.fontSize = 0.08

View File

@ -911,7 +911,8 @@ public class StickerPickerScreen: ViewController {
externalExpansionView: nil,
useOpaqueTheme: false,
hideBackground: true,
stateContext: nil
stateContext: nil,
addImage: nil
)
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
@ -1168,7 +1169,10 @@ public class StickerPickerScreen: ViewController {
externalExpansionView: nil,
useOpaqueTheme: false,
hideBackground: true,
stateContext: nil
stateContext: nil,
addImage: {
}
)
if let (layout, navigationHeight) = self.currentLayout {

View File

@ -1595,7 +1595,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
externalExpansionView: self.view,
useOpaqueTheme: false,
hideBackground: false,
stateContext: nil
stateContext: nil,
addImage: nil
)
}

View File

@ -688,7 +688,8 @@ final class AvatarEditorScreenComponent: Component {
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: true,
stateContext: nil
stateContext: nil,
addImage: nil
)
data.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
@ -816,7 +817,8 @@ final class AvatarEditorScreenComponent: Component {
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: true,
stateContext: nil
stateContext: nil,
addImage: nil
)
self.state?.updated(transition: .immediate)

View File

@ -81,6 +81,8 @@ public final class CameraButton: Component {
super.init(frame: frame)
self.isExclusiveTouch = true
self.addSubview(self.contentView)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)

View File

@ -377,6 +377,9 @@ private final class CameraScreenComponent: CombinedComponent {
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.0 {
return
}
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
return
}
self.lastFlipTimestamp = currentTimestamp
self.camera.togglePosition()
@ -391,6 +394,9 @@ private final class CameraScreenComponent: CombinedComponent {
if let lastDualCameraTimestamp = self.lastDualCameraTimestamp, currentTimestamp - lastDualCameraTimestamp < 1.5 {
return
}
if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.0 {
return
}
self.lastDualCameraTimestamp = currentTimestamp
let isEnabled = !self.cameraState.isDualCamEnabled
@ -531,123 +537,7 @@ private final class CameraScreenComponent: CombinedComponent {
controlsBottomInset = -48.0
}
}
let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
component: CameraButton(
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
}
controller.requestDismiss(animated: true)
}
).tagged(cancelButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + cancelButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
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: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
range: nil,
waitForCompletion: false
),
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
}
state.toggleFlashMode()
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCamSupported {
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 - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
}
if case .holding = state.cameraState.recording {
} else {
@ -771,6 +661,122 @@ private final class CameraScreenComponent: CombinedComponent {
.position(captureControlsPosition)
)
let topControlInset: CGFloat = 20.0
if case .none = state.cameraState.recording, !state.isTransitioning {
let cancelButton = cancelButton.update(
component: CameraButton(
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
}
controller.requestDismiss(animated: true)
}
).tagged(cancelButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(cancelButton
.position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + cancelButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
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: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
range: nil,
waitForCompletion: false
),
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
}
state.toggleFlashMode()
}
).tagged(flashButtonTag),
availableSize: CGSize(width: 40.0, height: 40.0),
transition: .immediate
)
context.add(flashButton
.position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0 - 5.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + flashButton.size.height / 2.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
if !isTablet && Camera.isDualCamSupported {
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 - topControlInset - flashButton.size.width / 2.0 - 52.0, y: max(environment.statusBarHeight + 5.0, environment.safeInsets.top + topControlInset) + dualButton.size.height / 2.0 + 1.0))
.appear(.default(scale: true))
.disappear(.default(scale: true))
)
}
}
if isTablet {
let flipButton = flipButton.update(
component: CameraButton(
@ -820,9 +826,9 @@ private final class CameraScreenComponent: CombinedComponent {
if isTablet {
timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0)
} else {
timePosition = CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)
timePosition = CGPoint(x: availableSize.width / 2.0, y: max(environment.statusBarHeight + 5.0 + 20.0, environment.safeInsets.top + topControlInset + 20.0))
}
if state.cameraState.recording != .none {
let timeBackground = timeBackground.update(
component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0),
@ -1037,7 +1043,7 @@ public class CameraScreen: ViewController {
}
}
fileprivate final class Node: ViewControllerTracingNode {
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: CameraScreen?
private let context: AccountContext
private let updateState: ActionSlot<CameraState>
@ -1166,7 +1172,6 @@ public class CameraScreen: ViewController {
if let self {
if modeChange != .none {
if case .dualCamera = modeChange, self.cameraPosition == .front {
} else {
if let snapshot = self.mainPreviewView.snapshotView(afterScreenUpdates: false) {
self.mainPreviewView.addSubview(snapshot)
@ -1277,7 +1282,13 @@ public class CameraScreen: ViewController {
self.mainPreviewContainerView.addSubview(cloneView)
}
} else {
if let cloneView = self.mainPreviewView.snapshotView(afterScreenUpdates: false) {
cloneView.frame = self.mainPreviewView.frame
self.additionalPreviewSnapshotView = cloneView
self.additionalPreviewContainerView.addSubview(cloneView)
}
if let cloneView = self.additionalPreviewView.snapshotView(afterScreenUpdates: false) {
cloneView.frame = self.additionalPreviewView.frame
self.mainPreviewSnapshotView = cloneView
self.mainPreviewContainerView.addSubview(cloneView)
}
@ -1297,6 +1308,7 @@ public class CameraScreen: ViewController {
self.changingPositionDisposable?.dispose()
}
private var pipPanGestureRecognizer: UIPanGestureRecognizer?
override func didLoad() {
super.didLoad()
@ -1304,26 +1316,47 @@ public class CameraScreen: ViewController {
self.view.disablesInteractiveKeyboardGestureRecognizer = true
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
self.mainPreviewContainerView.addGestureRecognizer(pinchGestureRecognizer)
self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
panGestureRecognizer.maximumNumberOfTouches = 1
self.mainPreviewContainerView.addGestureRecognizer(panGestureRecognizer)
self.previewContainerView.addGestureRecognizer(panGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.mainPreviewContainerView.addGestureRecognizer(tapGestureRecognizer)
self.previewContainerView.addGestureRecognizer(tapGestureRecognizer)
let doubleGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:)))
doubleGestureRecognizer.numberOfTapsRequired = 2
self.mainPreviewContainerView.addGestureRecognizer(doubleGestureRecognizer)
self.previewContainerView.addGestureRecognizer(doubleGestureRecognizer)
let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:)))
self.additionalPreviewContainerView.addGestureRecognizer(pipPanGestureRecognizer)
pipPanGestureRecognizer.delegate = self
self.previewContainerView.addGestureRecognizer(pipPanGestureRecognizer)
self.pipPanGestureRecognizer = pipPanGestureRecognizer
self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
self.camera.startCapture()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
return false
}
return true
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: gestureRecognizer.view)
if gestureRecognizer === self.pipPanGestureRecognizer {
if !self.isDualCamEnabled {
return false
}
return self.additionalPreviewContainerView.frame.contains(location)
}
return self.hasAppeared
}
@objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
switch gestureRecognizer.state {
case .changed:
@ -1334,7 +1367,7 @@ public class CameraScreen: ViewController {
break
}
}
private var isDismissing = false
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let controller = self.controller else {
@ -1469,7 +1502,9 @@ public class CameraScreen: ViewController {
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.requestUpdateLayout(hasAppeared: true, transition: .immediate)
})
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
let minSide = min(self.previewContainerView.bounds.width, self.previewContainerView.bounds.height)
@ -1977,7 +2012,7 @@ public class CameraScreen: ViewController {
additionalPreviewView = self.additionalPreviewView
}
let mainPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - mainPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((previewSize.height - mainPreviewInnerSize.height) / 2.0)), size: mainPreviewInnerSize)
let mainPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((previewFrame.width - mainPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((previewFrame.height - mainPreviewInnerSize.height) / 2.0)), size: mainPreviewInnerSize)
let additionalPreviewInnerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((circleSide - additionalPreviewInnerSize.width) / 2.0), y: floorToScreenPixels((circleSide - additionalPreviewInnerSize.height) / 2.0)), size: additionalPreviewInnerSize)
if mainPreviewView.superview != self.mainPreviewContainerView {

View File

@ -62,6 +62,8 @@ final class ModeComponent: Component {
init() {
super.init(frame: .zero)
self.isExclusiveTouch = true
self.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}

View File

@ -1311,7 +1311,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
externalExpansionView: nil,
useOpaqueTheme: false,
hideBackground: false,
stateContext: self.stateContext?.emojiState
stateContext: self.stateContext?.emojiState,
addImage: nil
)
self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
@ -1609,7 +1610,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
externalExpansionView: nil,
useOpaqueTheme: false,
hideBackground: false,
stateContext: nil
stateContext: nil,
addImage: nil
)
@ -2499,7 +2501,8 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
externalExpansionView: nil,
useOpaqueTheme: false,
hideBackground: hideBackground,
stateContext: nil
stateContext: nil,
addImage: nil
)
let semaphore = DispatchSemaphore(value: 0)

View File

@ -676,7 +676,8 @@ public final class EmojiStatusSelectionController: ViewController {
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil
stateContext: nil,
addImage: nil
)
strongSelf.refreshLayout(transition: .immediate)

View File

@ -2322,6 +2322,7 @@ public final class EmojiPagerContentComponent: Component {
public let hideBackground: Bool
public let scrollingStickersGridPromise = ValuePromise<Bool>(false)
public let stateContext: StateContext?
public let addImage: (() -> Void)?
public init(
performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void,
@ -2347,7 +2348,8 @@ public final class EmojiPagerContentComponent: Component {
externalExpansionView: UIView?,
useOpaqueTheme: Bool,
hideBackground: Bool,
stateContext: StateContext?
stateContext: StateContext?,
addImage: (() -> Void)?
) {
self.performItemAction = performItemAction
self.deleteBackwards = deleteBackwards
@ -2373,6 +2375,7 @@ public final class EmojiPagerContentComponent: Component {
self.useOpaqueTheme = useOpaqueTheme
self.hideBackground = hideBackground
self.stateContext = stateContext
self.addImage = addImage
}
}

View File

@ -409,7 +409,8 @@ public final class EmojiSearchContent: ASDisplayNode, EntitySearchContainerNode
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil
stateContext: nil,
addImage: nil
)
self.dataDisposable = (

View File

@ -690,6 +690,18 @@ public final class EntityKeyboardComponent: Component {
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
if let addImage = component.stickerContent?.inputInteractionHolder.inputInteraction?.addImage {
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "image", component: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Media Editor/AddImage",
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
maxSize: nil
)),
action: {
addImage()
}
).minSize(CGSize(width: 38.0, height: 38.0)))))
}
}
if let _ = deleteBackwards {

View File

@ -950,7 +950,8 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
externalExpansionView: nil,
useOpaqueTheme: true,
hideBackground: false,
stateContext: nil
stateContext: nil,
addImage: nil
)
}
}

View File

@ -85,7 +85,21 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
public var baseSize: CGSize {
let size = max(10.0, min(self.referenceDrawingSize.width, self.referenceDrawingSize.height) * 0.25)
return CGSize(width: size, height: size)
let dimensions: CGSize
switch self.content {
case let .image(image, _):
dimensions = image.size
case let .file(file):
dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
case let .video(_, image, _):
dimensions = image?.size ?? CGSize(width: 512.0, height: 512.0)
case .dualVideoReference:
dimensions = CGSize(width: 512.0, height: 512.0)
}
let boundingSize = CGSize(width: size, height: size)
return dimensions.fitted(boundingSize)
}
public var isAnimated: Bool {

View File

@ -269,6 +269,9 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII
var baseScale: CGFloat = 1.0
if let baseSize = entity.baseSize {
baseScale = baseSize.width / image.extent.width
if baseSize.width != baseSize.height {
baseScale *= min(baseSize.width, baseSize.height) / max(baseSize.width, baseSize.height)
}
}
var transform = CGAffineTransform.identity

View File

@ -126,6 +126,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
switch content {
case let .file(file):
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.isAnimated = true
self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm"
@ -133,7 +134,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker)
let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
if let source = self.source {
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fitToSize: CGSize
if self.isStatic {
fitToSize = CGSize(width: 768, height: 768)
@ -334,6 +334,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,

View File

@ -383,6 +383,9 @@ public final class MediaEditorValues: Codable, Equatable {
if self.videoTrimRange != nil {
return true
}
if self.drawing != nil {
return true
}
if !self.entities.isEmpty {
return true
}

View File

@ -930,10 +930,10 @@ final class MediaEditorScreenComponent: Component {
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - scrubberInset * 2.0, height: availableSize.height)
containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height)
)
let scrubberFrame = CGRect(origin: CGPoint(x: scrubberInset, y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0 + controlsBottomInset), size: scrubberSize)
let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height - 8.0 + controlsBottomInset), size: scrubberSize)
if let scrubberView = self.scrubber.view {
if scrubberView.superview == nil {
if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil {

View File

@ -928,6 +928,7 @@ public final class MessageInputPanelComponent: Component {
}
self.currentMediaInputIsVoice = !self.currentMediaInputIsVoice
self.hapticFeedback.impact(.medium)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
},
updateMediaCancelFraction: { [weak self] mediaCancelFraction in

View File

@ -139,6 +139,105 @@ private final class StoryLongPressRecognizer: UILongPressGestureRecognizer {
}
}
private final class StoryPinchGesture: UIPinchGestureRecognizer {
private final class Target {
var updated: (() -> Void)?
@objc func onGesture(_ gesture: UIPinchGestureRecognizer) {
self.updated?()
}
}
private let target: Target
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var began: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)?
private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
init() {
self.target = Target()
super.init(target: self.target, action: #selector(self.target.onGesture(_:)))
self.target.updated = { [weak self] in
self?.gestureUpdated()
}
}
override func reset() {
super.reset()
self.currentNumberOfTouches = 0
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
}
private func gestureUpdated() {
switch self.state {
case .began:
self.currentOffset = CGPoint()
let pinchLocation = self.location(in: self.view)
self.pinchLocation = pinchLocation
self.initialLocation = pinchLocation
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.currentNumberOfTouches = self.numberOfTouches
self.began?()
case .changed:
let locationSum = self.location(in: self.view)
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
}
self.currentNumberOfTouches = self.numberOfTouches
if let initialLocation = self.initialLocation {
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
}
if let (scale, pinchLocation, _) = self.currentTransform {
self.currentTransform = (scale, pinchLocation, self.currentOffset)
self.updated?(scale, pinchLocation, self.currentOffset)
}
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled:
self.ended?()
default:
break
}
}
}
private final class StoryContainerScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -351,7 +450,28 @@ private final class StoryContainerScreenComponent: Component {
self.longPressRecognizer = longPressRecognizer
self.addGestureRecognizer(longPressRecognizer)
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
let pinchRecognizer = StoryPinchGesture()
pinchRecognizer.delegate = self
pinchRecognizer.updated = { [weak self] scale, pinchLocation, offset in
guard let self else {
return
}
var pinchLocation = pinchLocation
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
pinchLocation = self.convert(pinchLocation, to: itemSetComponentView)
}
}
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: pinchLocation, offset: offset)
self.state?.updated(transition: .immediate)
}
pinchRecognizer.ended = { [weak self] in
guard let self else {
return
}
self.itemSetPinchState = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
}
self.addGestureRecognizer(pinchRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
@ -431,6 +551,13 @@ private final class StoryContainerScreenComponent: Component {
self.volumeButtonsListenerShouldBeActiveDisposable?.dispose()
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is StoryPinchGesture {
return !hasFirstResponder(self)
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return false
@ -581,10 +708,21 @@ private final class StoryContainerScreenComponent: Component {
@objc private func dismissPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.dismissAllTooltips()
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] {
if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
if itemSetComponentView.hasActiveDeactivateableInput() {
itemSetComponentView.deactivateInput()
recognizer.isEnabled = false
recognizer.isEnabled = true
return
}
}
}
self.verticalPanState = ItemSetPanState(fraction: 0.0, didBegin: true)
self.state?.updated(transition: .immediate)
self.dismissAllTooltips()
case .changed:
let translation = recognizer.translation(in: self)
self.verticalPanState = ItemSetPanState(fraction: max(-1.0, min(1.0, translation.y / self.bounds.height)), didBegin: true)
@ -656,34 +794,17 @@ private final class StoryContainerScreenComponent: Component {
guard case .recognized = recognizer.state else {
return
}
let location = recognizer.location(in: recognizer.view)
if let currentItemView = self.visibleItemSetViews.first?.value {
if location.x < currentItemView.frame.minX {
self.navigate(direction: .previous)
} else if location.x > currentItemView.frame.maxX {
self.navigate(direction: .next)
}
}
}
@objc private func pinchGesture(_ recognizer: UIPinchGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
let location = recognizer.location(in: self)
let scale = max(1.0, recognizer.scale)
if let itemSetPinchState = self.itemSetPinchState {
let offset = CGPoint(x: location.x - itemSetPinchState.location.x , y: location.y - itemSetPinchState.location.y)
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: itemSetPinchState.location, offset: offset)
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let currentItemView = itemSetView.view.view as? StoryItemSetContainerComponent.View {
if currentItemView.hasActiveDeactivateableInput() {
currentItemView.deactivateInput()
} else {
self.itemSetPinchState = StoryItemSetContainerComponent.PinchState(scale: scale, location: location, offset: .zero)
if location.x < currentItemView.frame.minX {
self.navigate(direction: .previous)
} else if location.x > currentItemView.frame.maxX {
self.navigate(direction: .next)
}
}
self.state?.updated(transition: .immediate)
case .cancelled, .ended:
self.itemSetPinchState = nil
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
default:
break
}
}
@ -996,6 +1117,7 @@ private final class StoryContainerScreenComponent: Component {
}
}
var presentationContextInsets = UIEdgeInsets()
if !currentSlices.isEmpty, let focusedIndex {
for i in max(0, focusedIndex - 1) ... min(focusedIndex + 1, currentSlices.count - 1) {
var isItemVisible = false
@ -1053,6 +1175,10 @@ private final class StoryContainerScreenComponent: Component {
itemSetContainerInsets.top = 0.0
itemSetContainerInsets.bottom = floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0)
itemSetContainerSafeInsets.bottom = 0.0
presentationContextInsets.left = floorToScreenPixels((availableSize.width - itemSetContainerSize.width) / 2.0)
presentationContextInsets.right = presentationContextInsets.left
presentationContextInsets.bottom = itemSetContainerInsets.bottom
}
let _ = itemSetView.view.update(
@ -1327,8 +1453,8 @@ private final class StoryContainerScreenComponent: Component {
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: contentDerivedBottomInset, right: 0.0),
safeInsets: UIEdgeInsets(),
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: contentDerivedBottomInset + presentationContextInsets.bottom, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: presentationContextInsets.left, bottom: 0.0, right: presentationContextInsets.right),
additionalInsets: UIEdgeInsets(),
statusBarHeight: nil,
inputHeight: nil,

View File

@ -663,25 +663,30 @@ public final class StoryItemSetContainerComponent: Component {
return false
}
private func deactivateInput() {
func hasActiveDeactivateableInput() -> Bool {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
if view.canDeactivateInput() && (hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media) {
return true
}
}
return false
}
func deactivateInput() {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View, view.canDeactivateInput() {
self.sendMessageContext.currentInputMode = .text
if hasFirstResponder(self) {
view.deactivateInput()
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
if hasFirstResponder(self) || self.sendMessageContext.currentInputMode == .media {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
if view.canDeactivateInput() {
self.sendMessageContext.currentInputMode = .text
if hasFirstResponder(self) {
view.deactivateInput()
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
} else {
view.animateError()
}
}
self.deactivateInput()
} else if self.displayViewList {
let point = recognizer.location(in: self)
@ -2273,7 +2278,7 @@ public final class StoryItemSetContainerComponent: Component {
let tooltipScreen = TooltipScreen(
account: component.context.account,
sharedContext: component.context.sharedContext,
text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: self).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
text: .plain(text: "This video has no sound"), style: .default, location: TooltipScreen.Location.point(soundButtonView.convert(soundButtonView.bounds, to: nil).offsetBy(dx: 1.0, dy: -10.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: true)
}
)
@ -3174,9 +3179,6 @@ public final class StoryItemSetContainerComponent: Component {
}
let subject: Signal<MediaEditorScreen.Subject?, NoError>
// if let source {
// subject = .single(.draft(source, Int64(id)))
// } else {
var duration: Double?
let media = item.media._asMedia()

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "photo_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,154 @@
%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 3.835083 3.834961 cm
1.000000 1.000000 1.000000 scn
5.465000 16.330017 m
5.436178 16.330017 l
5.436174 16.330017 l
4.620535 16.330023 3.967872 16.330027 3.440454 16.286936 c
2.899074 16.242702 2.431364 16.149773 2.001125 15.930555 c
1.311511 15.579180 0.750837 15.018506 0.399461 14.328892 c
0.180244 13.898653 0.087314 13.430943 0.043081 12.889563 c
-0.000010 12.362144 -0.000006 11.709482 0.000000 10.893843 c
0.000000 10.893839 l
0.000000 10.865017 l
0.000000 5.465017 l
0.000000 5.436195 l
-0.000006 4.620555 -0.000010 3.967890 0.043081 3.440471 c
0.087314 2.899090 0.180244 2.431380 0.399461 2.001142 c
0.750837 1.311528 1.311511 0.750854 2.001125 0.399478 c
2.431364 0.180260 2.899074 0.087330 3.440454 0.043098 c
3.967863 0.000008 4.620512 0.000011 5.436130 0.000017 c
5.436194 0.000017 l
5.465001 0.000017 l
10.865000 0.000017 l
10.893806 0.000017 l
10.893871 0.000017 l
11.709489 0.000011 12.362138 0.000008 12.889546 0.043098 c
13.430927 0.087330 13.898637 0.180260 14.328876 0.399478 c
15.018489 0.750854 15.579163 1.311528 15.930539 2.001142 c
16.149757 2.431380 16.242687 2.899091 16.286919 3.440471 c
16.330009 3.967880 16.330006 4.620529 16.330000 5.436146 c
16.330000 5.436212 l
16.330000 5.465017 l
16.330000 10.865017 l
16.330000 10.893824 l
16.330000 10.893888 l
16.330006 11.709505 16.330009 12.362154 16.286919 12.889563 c
16.242687 13.430943 16.149757 13.898653 15.930539 14.328892 c
15.579163 15.018506 15.018489 15.579180 14.328876 15.930555 c
13.898637 16.149773 13.430926 16.242702 12.889546 16.286936 c
12.362126 16.330029 11.709461 16.330023 10.893822 16.330017 c
10.865000 16.330017 l
5.465000 16.330017 l
h
2.604932 14.745517 m
2.816429 14.853280 3.089626 14.923841 3.548759 14.961352 c
4.015654 14.999499 4.613948 15.000017 5.465000 15.000017 c
10.865000 15.000017 l
11.716052 15.000017 12.314346 14.999499 12.781241 14.961352 c
13.240374 14.923841 13.513572 14.853280 13.725068 14.745517 c
14.164426 14.521652 14.521636 14.164443 14.745501 13.725084 c
14.853263 13.513588 14.923823 13.240391 14.961336 12.781259 c
14.999483 12.314363 15.000000 11.716068 15.000000 10.865017 c
15.000000 5.465017 l
15.000000 5.137442 14.999924 4.847313 14.997654 4.587710 c
12.903418 6.817746 l
12.230759 7.534022 11.087341 7.515037 10.438833 6.776826 c
9.645941 5.874260 l
5.897148 9.831320 l
5.226022 10.539732 4.092310 10.521740 3.444000 9.792391 c
1.330000 7.414142 l
1.330000 10.865017 l
1.330000 11.716068 1.330518 12.314363 1.368664 12.781259 c
1.406177 13.240391 1.476737 13.513588 1.584500 13.725084 c
1.808364 14.164443 2.165574 14.521652 2.604932 14.745517 c
h
11.933909 5.907277 m
14.833861 2.819281 l
14.807792 2.739742 14.778378 2.669474 14.745501 2.604949 c
14.521636 2.165591 14.164426 1.808381 13.725068 1.584517 c
13.714809 1.579343 l
10.564494 4.904676 l
11.438032 5.899043 l
11.568513 6.047573 11.798571 6.051393 11.933909 5.907277 c
h
1.330001 5.412228 m
1.330038 4.589128 1.331311 4.005958 1.368664 3.548775 c
1.406177 3.089643 1.476737 2.816445 1.584500 2.604949 c
1.808364 2.165591 2.165574 1.808381 2.604932 1.584517 c
2.816429 1.476754 3.089626 1.406194 3.548759 1.368681 c
4.015654 1.330534 4.613949 1.330017 5.465001 1.330017 c
10.865000 1.330017 l
11.357291 1.330017 11.765009 1.330190 12.111642 1.337719 c
4.931631 8.916620 l
4.796600 9.059153 4.568496 9.055533 4.438055 8.908787 c
1.330001 5.412228 l
h
11.664969 10.165024 m
12.493397 10.165024 13.164969 10.836597 13.164969 11.665024 c
13.164969 12.493451 12.493397 13.165024 11.664969 13.165024 c
10.836542 13.165024 10.164969 12.493451 10.164969 11.665024 c
10.164969 10.836597 10.836542 10.165024 11.664969 10.165024 c
h
f*
n
Q
endstream
endobj
3 0 obj
3694
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003784 00000 n
0000003807 00000 n
0000003980 00000 n
0000004054 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4113
%%EOF

View File

@ -8067,7 +8067,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .proxy:
self.controller?.push(proxySettingsController(context: self.context))
case .stories:
self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
case .savedMessages:
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
@ -8247,7 +8247,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .emojiStatus:
self.headerNode.invokeDisplayPremiumIntro()
case .powerSaving:
self.controller?.push(energySavingSettingsScreen(context: self.context))
push(energySavingSettingsScreen(context: self.context))
}
}