mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-08 19:10:53 +00:00
Camera and editor improvements
This commit is contained in:
parent
9bd6dd35c7
commit
2b85ec7b5f
@ -21,11 +21,13 @@ public final class LottieAnimationComponent: Component {
|
||||
public var name: String
|
||||
public var mode: Mode
|
||||
public var range: (CGFloat, CGFloat)?
|
||||
public var waitForCompletion: Bool
|
||||
|
||||
public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil) {
|
||||
public init(name: String, mode: Mode, range: (CGFloat, CGFloat)? = nil, waitForCompletion: Bool = true) {
|
||||
self.name = name
|
||||
self.mode = mode
|
||||
self.range = range
|
||||
self.waitForCompletion = waitForCompletion
|
||||
}
|
||||
|
||||
public static func == (lhs: LottieAnimationComponent.AnimationItem, rhs: LottieAnimationComponent.AnimationItem) -> Bool {
|
||||
@ -157,7 +159,7 @@ public final class LottieAnimationComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let animationView = self.animationView, animationView.isAnimationPlaying {
|
||||
if let animationView = self.animationView, animationView.isAnimationPlaying && component.animation.waitForCompletion {
|
||||
updateComponent = false
|
||||
self.currentCompletion = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -149,10 +149,10 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
}
|
||||
|
||||
public static func encodeEntities(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> Data? {
|
||||
public static func encodeEntities(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> [CodableDrawingEntity] {
|
||||
let entities = entities
|
||||
guard !entities.isEmpty else {
|
||||
return nil
|
||||
return []
|
||||
}
|
||||
if let entitiesView {
|
||||
for entity in entities {
|
||||
@ -161,7 +161,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
}
|
||||
}
|
||||
let codableEntities = entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
return entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
}
|
||||
|
||||
public static func encodeEntitiesData(_ entities: [DrawingEntity], entitiesView: DrawingEntitiesView? = nil) -> Data? {
|
||||
let codableEntities = encodeEntities(entities, entitiesView: entitiesView)
|
||||
if let data = try? JSONEncoder().encode(codableEntities) {
|
||||
return data
|
||||
} else {
|
||||
@ -170,7 +174,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
|
||||
var entitiesData: Data? {
|
||||
return DrawingEntitiesView.encodeEntities(self.entities, entitiesView: self)
|
||||
return DrawingEntitiesView.encodeEntitiesData(self.entities, entitiesView: self)
|
||||
}
|
||||
|
||||
var hasChanges: Bool {
|
||||
|
@ -2392,6 +2392,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.entitiesView.selectEntity(nil)
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: topGradientTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
@ -2797,15 +2799,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
}
|
||||
|
||||
public func animateOut(_ completion: @escaping (() -> Void)) {
|
||||
self.selectionContainerView.alpha = 0.0
|
||||
self.entitiesView.selectEntity(nil)
|
||||
|
||||
self.node.animateOut(completion: {
|
||||
completion()
|
||||
})
|
||||
//
|
||||
// Queue.mainQueue().after(0.4) {
|
||||
// self.node.isHidden = true
|
||||
// }
|
||||
}
|
||||
|
||||
private var orientation: UIInterfaceOrientation?
|
||||
@ -2932,7 +2930,7 @@ public final class DrawingToolsInteraction {
|
||||
self.activate()
|
||||
}
|
||||
|
||||
func activate() {
|
||||
public func activate() {
|
||||
self.isActive = true
|
||||
|
||||
self.entitiesView.selectionContainerView = self.selectionContainerView
|
||||
|
@ -75,11 +75,11 @@ final class MediaPickerGridItem: GridItem {
|
||||
}
|
||||
}
|
||||
|
||||
private let maskImage = generateImage(CGSize(width: 1.0, height: 24.0), opaque: false, rotatedContext: { size, context in
|
||||
private let maskImage = generateImage(CGSize(width: 1.0, height: 36.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.6).cgColor] as CFArray
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.45).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
@ -93,8 +93,10 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
var currentState: (PHFetchResult<PHAsset>, Int)?
|
||||
var currentDraftState: (MediaEditorDraft, Int)?
|
||||
var enableAnimations: Bool = true
|
||||
var stories: Bool = false
|
||||
private var selectable: Bool = false
|
||||
|
||||
private let backgroundNode: ASImageNode
|
||||
private let imageNode: ImageNode
|
||||
private var checkNode: InteractiveCheckNode?
|
||||
private let gradientNode: ASImageNode
|
||||
@ -115,6 +117,9 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
var selected: (() -> Void)?
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.contentMode = .scaleToFill
|
||||
|
||||
self.imageNode = ImageNode()
|
||||
self.imageNode.clipsToBounds = true
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
@ -273,8 +278,15 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
self.enableAnimations = enableAnimations
|
||||
self.stories = stories
|
||||
|
||||
self.backgroundColor = theme.list.mediaPlaceholderColor
|
||||
|
||||
if stories {
|
||||
if self.backgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.backgroundNode, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index {
|
||||
self.currentMediaState = (media.asset, index)
|
||||
@ -290,9 +302,16 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
self.enableAnimations = enableAnimations
|
||||
self.stories = stories
|
||||
|
||||
self.backgroundColor = theme.list.mediaPlaceholderColor
|
||||
|
||||
if stories {
|
||||
if self.backgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.backgroundNode, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index {
|
||||
let editingContext = interaction.editingState
|
||||
let asset = fetchResult.object(at: index)
|
||||
@ -334,6 +353,27 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
|
||||
if stories {
|
||||
self.imageNode.contentUpdated = { [weak self] image in
|
||||
if let self {
|
||||
if self.backgroundNode.image == nil {
|
||||
if let image, image.size.width > image.size.height {
|
||||
self.imageNode.contentMode = .scaleAspectFit
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: [colors.0, colors.1])
|
||||
Queue.mainQueue().async {
|
||||
self.backgroundNode.image = gradientImage
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let originalSignal = assetImageSignal //assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, synchronous: true)
|
||||
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
||||
|> mapToSignal { result in
|
||||
@ -344,7 +384,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
}
|
||||
self.imageNode.setSignal(imageSignal)
|
||||
|
||||
|
||||
let spoilerSignal = Signal<Bool, NoError> { subscriber in
|
||||
if let signal = editingContext.spoilerSignal(forIdentifier: asset.localIdentifier) {
|
||||
let disposable = signal.start(next: { next in
|
||||
@ -443,8 +483,9 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
self.imageNode.frame = self.bounds.insetBy(dx: -1.0 + UIScreenPixel, dy: -1.0 + UIScreenPixel)
|
||||
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 24.0, width: self.bounds.width, height: 24.0)
|
||||
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 36.0, width: self.bounds.width, height: 36.0)
|
||||
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
|
||||
self.activateAreaNode.frame = self.bounds
|
||||
|
||||
@ -478,7 +519,20 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
func transitionImage() -> UIImage? {
|
||||
return self.imageNode.image
|
||||
if let backgroundImage = self.backgroundNode.image {
|
||||
return generateImage(self.bounds.size, contextGenerator: { size, context in
|
||||
if let cgImage = backgroundImage.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
if let image = self.imageNode.image, let cgImage = image.cgImage {
|
||||
let fittedSize = image.size.fitted(size)
|
||||
let fittedFrame = CGRect(origin: CGPoint(x: (size.width - fittedSize.width) / 2.0, y: (size.height - fittedSize.height) / 2.0), size: fittedSize)
|
||||
context.draw(cgImage, in: fittedFrame)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return self.imageNode.image
|
||||
}
|
||||
}
|
||||
|
||||
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
|
@ -1811,11 +1811,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
return
|
||||
}
|
||||
|
||||
self.requestAttachmentMenuExpansion()
|
||||
|
||||
var embedded = true
|
||||
if case .story = mode {
|
||||
embedded = false
|
||||
} else {
|
||||
self.requestAttachmentMenuExpansion()
|
||||
}
|
||||
|
||||
var updateNavigationStackImpl: ((AttachmentContainable) -> Void)?
|
||||
|
@ -1410,7 +1410,7 @@ final class AvatarEditorScreenComponent: Component {
|
||||
|
||||
let colors: [NSNumber] = state.selectedBackground.colors.map { Int32(bitPattern: $0) as NSNumber }
|
||||
|
||||
let entitiesData = DrawingEntitiesView.encodeEntities([entity])
|
||||
let entitiesData = DrawingEntitiesView.encodeEntitiesData([entity])
|
||||
|
||||
let paintingData = TGPaintingData(
|
||||
drawing: nil,
|
||||
|
@ -144,6 +144,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
var cameraState = CameraState(mode: .photo, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0)
|
||||
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot<Signal<CameraScreen.Result, NoError>>) {
|
||||
self.context = context
|
||||
self.camera = camera
|
||||
@ -193,6 +195,22 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
func toggleFlashMode() {
|
||||
if self.cameraState.flashMode == .off {
|
||||
self.camera.setFlashMode(.on)
|
||||
} else if self.cameraState.flashMode == .on {
|
||||
self.camera.setFlashMode(.auto)
|
||||
} else {
|
||||
self.camera.setFlashMode(.off)
|
||||
}
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
}
|
||||
|
||||
func togglePosition() {
|
||||
self.camera.togglePosition()
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
}
|
||||
|
||||
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
||||
guard hint != self.swipeHint else {
|
||||
return
|
||||
@ -330,7 +348,8 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: flashIconName,
|
||||
mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
|
||||
range: nil
|
||||
range: nil,
|
||||
waitForCompletion: false
|
||||
),
|
||||
colors: [:],
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
@ -356,13 +375,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
if state.cameraState.flashMode == .off {
|
||||
state.camera.setFlashMode(.on)
|
||||
} else if state.cameraState.flashMode == .on {
|
||||
state.camera.setFlashMode(.auto)
|
||||
} else {
|
||||
state.camera.setFlashMode(.off)
|
||||
}
|
||||
state.toggleFlashMode()
|
||||
}
|
||||
).tagged(flashButtonTag),
|
||||
availableSize: CGSize(width: 40.0, height: 40.0),
|
||||
@ -453,7 +466,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
guard let state else {
|
||||
return
|
||||
}
|
||||
state.camera.togglePosition()
|
||||
state.togglePosition()
|
||||
},
|
||||
galleryTapped: {
|
||||
guard let controller = environment.controller() as? CameraScreen else {
|
||||
@ -882,7 +895,7 @@ public class CameraScreen: ViewController {
|
||||
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 {
|
||||
controller.presentGallery()
|
||||
controller.presentGallery(fromGesture: true)
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
}
|
||||
@ -1239,6 +1252,8 @@ public class CameraScreen: ViewController {
|
||||
|
||||
private var audioSessionDisposable: Disposable?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
mode: Mode,
|
||||
@ -1289,7 +1304,11 @@ public class CameraScreen: ViewController {
|
||||
self.node.animateInFromEditor(toGallery: self.galleryController != nil)
|
||||
}
|
||||
|
||||
func presentGallery() {
|
||||
func presentGallery(fromGesture: Bool = false) {
|
||||
if !fromGesture {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
}
|
||||
|
||||
var didStopCameraCapture = false
|
||||
let stopCameraCapture = { [weak self] in
|
||||
guard !didStopCameraCapture, let self else {
|
||||
@ -1344,9 +1363,15 @@ public class CameraScreen: ViewController {
|
||||
guard !self.isDismissed else {
|
||||
return
|
||||
}
|
||||
|
||||
if !interactive {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
}
|
||||
|
||||
self.node.camera.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
if animated {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
if !interactive {
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
|
||||
@ -1360,9 +1385,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var isTransitioning = false
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
self.isTransitioning = true
|
||||
let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0)
|
||||
transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
@ -1382,7 +1405,6 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
self.isTransitioning = false
|
||||
if dismissing {
|
||||
if transitionFraction < 0.7 || velocity < -1000.0 {
|
||||
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
|
||||
|
@ -107,6 +107,8 @@ public final class MediaEditor {
|
||||
}
|
||||
private let playerPlaybackStatePromise = Promise<(Double, Double, Bool)>((0.0, 0.0, false))
|
||||
|
||||
public var onFirstDisplay: () -> Void = {}
|
||||
|
||||
public func playerState(framesCount: Int) -> Signal<MediaEditorPlayerState?, NoError> {
|
||||
return self.playerPromise.get()
|
||||
|> mapToSignal { [weak self] player in
|
||||
@ -228,6 +230,7 @@ public final class MediaEditor {
|
||||
gradientColors: nil,
|
||||
videoTrimRange: nil,
|
||||
videoIsMuted: false,
|
||||
videoIsFullHd: false,
|
||||
drawing: nil,
|
||||
entities: [],
|
||||
toolValues: [:]
|
||||
@ -267,21 +270,11 @@ public final class MediaEditor {
|
||||
if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess {
|
||||
print("error")
|
||||
}
|
||||
|
||||
func gradientColors(from image: UIImage) -> (UIColor, UIColor) {
|
||||
let context = DrawingContext(size: CGSize(width: 1.0, height: 4.0), scale: 1.0, clear: false)!
|
||||
context.withFlippedContext({ context in
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 4.0))
|
||||
}
|
||||
})
|
||||
return (context.colorAt(CGPoint(x: 0.0, y: 0.0)), context.colorAt(CGPoint(x: 0.0, y: 3.0)))
|
||||
}
|
||||
|
||||
|
||||
let textureSource: Signal<(TextureSource, UIImage?, AVPlayer?, UIColor, UIColor), NoError>
|
||||
switch subject {
|
||||
case let .image(image, _):
|
||||
let colors = gradientColors(from: image)
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1))
|
||||
case let .draft(draft):
|
||||
guard let image = UIImage(contentsOfFile: draft.path) else {
|
||||
@ -291,7 +284,7 @@ public final class MediaEditor {
|
||||
if let gradientColors = draft.values.gradientColors {
|
||||
colors = (gradientColors.first!, gradientColors.last!)
|
||||
} else {
|
||||
colors = gradientColors(from: image)
|
||||
colors = mediaEditorGetGradientColors(from: image)
|
||||
}
|
||||
textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1))
|
||||
case let .video(path, _):
|
||||
@ -304,7 +297,7 @@ public final class MediaEditor {
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
if let image {
|
||||
let colors = gradientColors(from: UIImage(cgImage: image))
|
||||
let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image))
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1))
|
||||
} else {
|
||||
subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black))
|
||||
@ -329,7 +322,7 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
if !degraded {
|
||||
let colors = gradientColors(from: image)
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in
|
||||
if let asset {
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
@ -359,7 +352,7 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
if !degraded {
|
||||
let colors = gradientColors(from: image)
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
subscriber.putNext((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
@ -377,7 +370,7 @@ public final class MediaEditor {
|
||||
if let self {
|
||||
let (source, image, player, topColor, bottomColor) = sourceAndColors
|
||||
self.renderer.onNextRender = { [weak self] in
|
||||
self?.previewView?.removeTransitionImage()
|
||||
self?.onFirstDisplay()
|
||||
}
|
||||
self.renderer.textureSource = source
|
||||
self.player = player
|
||||
@ -446,6 +439,10 @@ public final class MediaEditor {
|
||||
self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted)
|
||||
}
|
||||
|
||||
public func setVideoIsFullHd(_ videoIsFullHd: Bool) {
|
||||
self.values = self.values.withUpdatedVideoIsFullHd(videoIsFullHd)
|
||||
}
|
||||
|
||||
private var targetTimePosition: (CMTime, Bool)?
|
||||
private var updatingTimePosition = false
|
||||
public func seek(_ position: Double, andPlay play: Bool) {
|
||||
|
@ -12,7 +12,7 @@ import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
|
||||
func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
|
||||
public func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
let gradientColors = colors.map { $0.cgColor } as CFArray
|
||||
@ -29,6 +29,16 @@ func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImag
|
||||
return image
|
||||
}
|
||||
|
||||
public func mediaEditorGetGradientColors(from image: UIImage) -> (UIColor, UIColor) {
|
||||
let context = DrawingContext(size: CGSize(width: 5.0, height: 5.0), scale: 1.0, clear: false)!
|
||||
context.withFlippedContext({ context in
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0))
|
||||
}
|
||||
})
|
||||
return (context.colorAt(CGPoint(x: 2.0, y: 0.0)), context.colorAt(CGPoint(x: 2.0, y: 4.0)))
|
||||
}
|
||||
|
||||
final class MediaEditorComposer {
|
||||
let device: MTLDevice?
|
||||
private let colorSpace: CGColorSpace
|
||||
@ -201,7 +211,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
|
||||
var initialScale: CGFloat
|
||||
if mediaImage.extent.height > mediaImage.extent.width {
|
||||
initialScale = dimensions.height / mediaImage.extent.height
|
||||
initialScale = max(dimensions.width / mediaImage.extent.width, dimensions.height / mediaImage.extent.height)
|
||||
} else {
|
||||
initialScale = dimensions.width / mediaImage.extent.width
|
||||
}
|
||||
|
@ -67,33 +67,4 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
self.renderer?.renderFrame()
|
||||
}
|
||||
|
||||
private var transitionView: UIImageView?
|
||||
public func setTransitionImage(_ image: UIImage) {
|
||||
self.transitionView?.removeFromSuperview()
|
||||
|
||||
let transitionView = UIImageView(image: image)
|
||||
transitionView.frame = self.bounds
|
||||
self.addSubview(transitionView)
|
||||
|
||||
self.transitionView = transitionView
|
||||
}
|
||||
|
||||
public func removeTransitionImage() {
|
||||
if let transitionView = self.transitionView {
|
||||
// transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak transitionView] _ in
|
||||
//
|
||||
// })
|
||||
transitionView.removeFromSuperview()
|
||||
self.transitionView = nil
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if let transitionView = self.transitionView {
|
||||
transitionView.frame = self.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
}
|
||||
if self.renderTarget != nil {
|
||||
let _ = self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
self.finalTexture = texture
|
||||
|
||||
@ -191,14 +191,30 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
if let self {
|
||||
self.semaphore.signal()
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
if let onNextRender = self.onNextRender {
|
||||
self.onNextRender = nil
|
||||
Queue.mainQueue().async {
|
||||
onNextRender()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
#else
|
||||
if let renderTarget = self.renderTarget, let drawable = renderTarget.drawable {
|
||||
drawable.addPresentedHandler { [weak self] _ in
|
||||
if let self, let onNextRender = self.onNextRender {
|
||||
self.onNextRender = nil
|
||||
Queue.mainQueue().async {
|
||||
onNextRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if let _ = self.renderTarget {
|
||||
commandBuffer.commit()
|
||||
|
@ -51,6 +51,7 @@ public final class MediaEditorValues: Codable {
|
||||
|
||||
case videoTrimRange
|
||||
case videoIsMuted
|
||||
case videoIsFullHd
|
||||
|
||||
case drawing
|
||||
case entities
|
||||
@ -68,6 +69,7 @@ public final class MediaEditorValues: Codable {
|
||||
|
||||
public let videoTrimRange: Range<Double>?
|
||||
public let videoIsMuted: Bool
|
||||
public let videoIsFullHd: Bool
|
||||
|
||||
public let drawing: UIImage?
|
||||
public let entities: [CodableDrawingEntity]
|
||||
@ -83,6 +85,7 @@ public final class MediaEditorValues: Codable {
|
||||
gradientColors: [UIColor]?,
|
||||
videoTrimRange: Range<Double>?,
|
||||
videoIsMuted: Bool,
|
||||
videoIsFullHd: Bool,
|
||||
drawing: UIImage?,
|
||||
entities: [CodableDrawingEntity],
|
||||
toolValues: [EditorToolKey: Any]
|
||||
@ -96,6 +99,7 @@ public final class MediaEditorValues: Codable {
|
||||
self.gradientColors = gradientColors
|
||||
self.videoTrimRange = videoTrimRange
|
||||
self.videoIsMuted = videoIsMuted
|
||||
self.videoIsFullHd = videoIsFullHd
|
||||
self.drawing = drawing
|
||||
self.entities = entities
|
||||
self.toolValues = toolValues
|
||||
@ -122,6 +126,7 @@ public final class MediaEditorValues: Codable {
|
||||
|
||||
self.videoTrimRange = try container.decodeIfPresent(Range<Double>.self, forKey: .videoTrimRange)
|
||||
self.videoIsMuted = try container.decode(Bool.self, forKey: .videoIsMuted)
|
||||
self.videoIsFullHd = try container.decode(Bool.self, forKey: .videoIsFullHd)
|
||||
|
||||
if let drawingData = try container.decodeIfPresent(Data.self, forKey: .drawing), let image = UIImage(data: drawingData) {
|
||||
self.drawing = image
|
||||
@ -175,31 +180,35 @@ public final class MediaEditorValues: Codable {
|
||||
}
|
||||
|
||||
public func makeCopy() -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: offset, cropSize: self.cropSize, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedVideoIsMuted(_ videoIsMuted: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedVideoIsFullHd(_ videoIsFullHd: Bool) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedVideoTrimRange(_ videoTrimRange: Range<Double>) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedDrawingAndEntities(drawing: UIImage?, entities: [CodableDrawingEntity]) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: drawing, entities: entities, toolValues: self.toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: drawing, entities: entities, toolValues: self.toolValues)
|
||||
}
|
||||
|
||||
func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues {
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, drawing: self.drawing, entities: self.entities, toolValues: toolValues)
|
||||
return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: toolValues)
|
||||
}
|
||||
}
|
||||
|
||||
@ -931,30 +940,40 @@ private let hasHEVCHardwareEncoder: Bool = {
|
||||
return result == noErr
|
||||
}()
|
||||
|
||||
public func recommendedVideoExportConfiguration(values: MediaEditorValues, frameRate: Float) -> MediaEditorVideoExport.Configuration {
|
||||
public func recommendedVideoExportConfiguration(values: MediaEditorValues, forceFullHd: Bool = false, frameRate: Float) -> MediaEditorVideoExport.Configuration {
|
||||
let compressionProperties: [String: Any]
|
||||
let codecType: AVVideoCodecType
|
||||
|
||||
if hasHEVCHardwareEncoder {
|
||||
codecType = AVVideoCodecType.hevc
|
||||
compressionProperties = [
|
||||
AVVideoAverageBitRateKey: 2000000,
|
||||
AVVideoAverageBitRateKey: 3800000,
|
||||
AVVideoProfileLevelKey: kVTProfileLevel_HEVC_Main_AutoLevel
|
||||
]
|
||||
} else {
|
||||
codecType = AVVideoCodecType.h264
|
||||
compressionProperties = [
|
||||
AVVideoAverageBitRateKey: 2000000,
|
||||
AVVideoAverageBitRateKey: 3800000,
|
||||
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
||||
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
|
||||
]
|
||||
}
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
if values.videoIsFullHd {
|
||||
width = 1080
|
||||
height = 1920
|
||||
} else {
|
||||
width = 720
|
||||
height = 1280
|
||||
}
|
||||
|
||||
let videoSettings: [String: Any] = [
|
||||
AVVideoCodecKey: codecType,
|
||||
AVVideoCompressionPropertiesKey: compressionProperties,
|
||||
AVVideoWidthKey: 720,
|
||||
AVVideoHeightKey: 1280
|
||||
AVVideoWidthKey: width,
|
||||
AVVideoHeightKey: height
|
||||
]
|
||||
|
||||
let audioSettings: [String: Any] = [
|
||||
|
@ -131,6 +131,7 @@ class DefaultRenderPass: RenderPass {
|
||||
final class OutputRenderPass: DefaultRenderPass {
|
||||
weak var renderTarget: RenderTarget?
|
||||
|
||||
@discardableResult
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else {
|
||||
return nil
|
||||
@ -154,10 +155,9 @@ final class OutputRenderPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let drawable = renderTarget.drawable {
|
||||
if let drawable = renderTarget.drawable {
|
||||
commandBuffer.present(drawable)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/TooltipUI",
|
||||
|
@ -22,6 +22,7 @@ import AvatarNode
|
||||
import ShareWithPeersScreen
|
||||
import PresentationDataUtils
|
||||
import ContextUI
|
||||
import BundleIconComponent
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -172,6 +173,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
private let privacyButton = ComponentView<Empty>()
|
||||
private let muteButton = ComponentView<Empty>()
|
||||
private let saveButton = ComponentView<Empty>()
|
||||
private let settingsButton = ComponentView<Empty>()
|
||||
|
||||
private var component: MediaEditorScreenComponent?
|
||||
private weak var state: State?
|
||||
@ -235,6 +237,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.settingsButton.view {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
@ -284,6 +291,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.settingsButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
@ -337,6 +349,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.settingsButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
}
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
@ -389,6 +406,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setScale(view: view, scale: 1.0)
|
||||
}
|
||||
|
||||
if let view = self.settingsButton.view {
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
transition.setScale(view: view, scale: 1.0)
|
||||
}
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
|
||||
@ -866,6 +888,44 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = state.playerState {
|
||||
let settingsButtonSize = self.settingsButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputSettingsIcon",
|
||||
tintColor: UIColor(rgb: 0xffffff)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
if let controller = environment.controller() as? MediaEditorScreen {
|
||||
controller.requestSettings()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let settingsButtonFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels((availableSize.width - settingsButtonSize.width) / 2.0), y: environment.safeInsets.top + 20.0 - inputPanelOffset),
|
||||
size: settingsButtonSize
|
||||
)
|
||||
if let settingsButtonView = self.settingsButton.view {
|
||||
if settingsButtonView.superview == nil {
|
||||
settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||
settingsButtonView.layer.shadowRadius = 4.0
|
||||
settingsButtonView.layer.shadowColor = UIColor.black.cgColor
|
||||
settingsButtonView.layer.shadowOpacity = 0.2
|
||||
self.addSubview(settingsButtonView)
|
||||
}
|
||||
transition.setPosition(view: settingsButtonView, position: settingsButtonFrame.center)
|
||||
transition.setBounds(view: settingsButtonView, bounds: CGRect(origin: .zero, size: settingsButtonFrame.size))
|
||||
transition.setScale(view: settingsButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0)
|
||||
transition.setAlpha(view: settingsButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
@ -948,6 +1008,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
|
||||
private let previewContainerView: UIView
|
||||
private var transitionInView: UIImageView?
|
||||
|
||||
private let gradientView: UIImageView
|
||||
private var gradientColorsDisposable: Disposable?
|
||||
@ -1096,7 +1157,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize)
|
||||
mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
|
||||
if fittedSize.height > fittedSize.width {
|
||||
mediaEntity.scale = storyDimensions.height / fittedSize.height
|
||||
mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height)
|
||||
} else {
|
||||
mediaEntity.scale = storyDimensions.width / fittedSize.width
|
||||
}
|
||||
@ -1238,10 +1299,17 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
@objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
if self.entitiesView.hasSelection {
|
||||
self.entitiesView.selectEntity(nil)
|
||||
let location = gestureRecognizer.location(in: self.view)
|
||||
var entitiesHitTestResult = self.entitiesView.hitTest(self.view.convert(location, to: self.entitiesView), with: nil)
|
||||
if entitiesHitTestResult is DrawingMediaEntityView {
|
||||
entitiesHitTestResult = nil
|
||||
}
|
||||
if entitiesHitTestResult == nil {
|
||||
if self.entitiesView.hasSelection {
|
||||
self.entitiesView.selectEntity(nil)
|
||||
}
|
||||
self.view.endEditing(true)
|
||||
}
|
||||
self.view.endEditing(true)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
@ -1252,9 +1320,27 @@ public final class MediaEditorScreen: ViewController {
|
||||
view.animateIn(from: .camera)
|
||||
}
|
||||
case let .gallery(transitionIn):
|
||||
if let transitionImage = transitionIn.sourceImage {
|
||||
if let sourceImage = transitionIn.sourceImage {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
self.previewView.setTransitionImage(transitionImage)
|
||||
|
||||
let transitionInView = UIImageView(image: sourceImage)
|
||||
var initialScale: CGFloat
|
||||
if sourceImage.size.height > sourceImage.size.width {
|
||||
initialScale = max(self.previewContainerView.bounds.width / sourceImage.size.width, self.previewContainerView.bounds.height / sourceImage.size.height)
|
||||
} else {
|
||||
initialScale = self.previewContainerView.bounds.width / sourceImage.size.width
|
||||
}
|
||||
transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale)
|
||||
self.previewContainerView.addSubview(transitionInView)
|
||||
self.transitionInView = transitionInView
|
||||
|
||||
self.mediaEditor?.onFirstDisplay = { [weak self] in
|
||||
if let self, let transitionInView = self.transitionInView {
|
||||
transitionInView.removeFromSuperview()
|
||||
self.transitionInView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if let sourceView = transitionIn.sourceView {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
@ -1265,7 +1351,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
let sourceAspectRatio = sourceLocalFrame.height / sourceLocalFrame.width
|
||||
|
||||
let duration: Double = 0.5
|
||||
let duration: Double = 0.4
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
@ -1311,7 +1397,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
let transitionOutView = UIImageView(image: sourceImage)
|
||||
var initialScale: CGFloat
|
||||
if sourceImage.size.height > sourceImage.size.width {
|
||||
initialScale = self.previewContainerView.bounds.height / sourceImage.size.height
|
||||
initialScale = max(self.previewContainerView.bounds.width / sourceImage.size.width, self.previewContainerView.bounds.height / sourceImage.size.height)
|
||||
} else {
|
||||
initialScale = self.previewContainerView.bounds.width / sourceImage.size.width
|
||||
}
|
||||
@ -1560,6 +1646,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.interaction?.insertEntity(textEntity)
|
||||
return
|
||||
case .drawing:
|
||||
self.interaction?.deactivate()
|
||||
let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData)
|
||||
self.drawingScreen = controller
|
||||
self.drawingView.isUserInteractionEnabled = true
|
||||
@ -1573,6 +1660,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.animateInFromTool()
|
||||
|
||||
self?.entitiesView.selectEntity(nil)
|
||||
self?.interaction?.activate()
|
||||
}
|
||||
controller.requestApply = { [weak controller, weak self] in
|
||||
self?.drawingScreen = nil
|
||||
@ -1589,6 +1677,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
self?.entitiesView.selectEntity(nil)
|
||||
self?.interaction?.activate()
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.animateOutToTool()
|
||||
@ -2020,7 +2109,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
mediaEditor.stop()
|
||||
|
||||
let codableEntities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
@ -2100,7 +2190,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let codableEntities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4"
|
||||
@ -2169,7 +2260,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, frameRate: 60.0)
|
||||
let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, forceFullHd: true, frameRate: 60.0)
|
||||
let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4"
|
||||
let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath)
|
||||
self.videoExport = videoExport
|
||||
@ -2201,6 +2292,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func requestSettings() {
|
||||
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
@ -15,6 +15,20 @@ private let borderHeight: CGFloat = 1.0 + UIScreenPixel
|
||||
private let frameWidth: CGFloat = 24.0
|
||||
private let minumumDuration: CGFloat = 1.0
|
||||
|
||||
private class VideoFrameLayer: SimpleShapeLayer {
|
||||
private let stripeLayer = SimpleShapeLayer()
|
||||
|
||||
override func layoutSublayers() {
|
||||
super.layoutSublayers()
|
||||
|
||||
if self.stripeLayer.superlayer == nil {
|
||||
self.stripeLayer.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.3).cgColor
|
||||
self.addSublayer(self.stripeLayer)
|
||||
}
|
||||
self.stripeLayer.frame = CGRect(x: self.bounds.width - UIScreenPixel, y: 0.0, width: UIScreenPixel, height: self.bounds.height)
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoScrubberComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
@ -87,8 +101,8 @@ final class VideoScrubberComponent: Component {
|
||||
private let transparentFramesContainer = UIView()
|
||||
private let opaqueFramesContainer = UIView()
|
||||
|
||||
private var transparentFrameLayers: [CALayer] = []
|
||||
private var opaqueFrameLayers: [CALayer] = []
|
||||
private var transparentFrameLayers: [VideoFrameLayer] = []
|
||||
private var opaqueFrameLayers: [VideoFrameLayer] = []
|
||||
|
||||
private var component: VideoScrubberComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
@ -118,6 +132,15 @@ final class VideoScrubberComponent: Component {
|
||||
context.fillPath()
|
||||
})?.withRenderingMode(.alwaysTemplate)
|
||||
|
||||
let positionImage = generateImage(CGSize(width: 2.0, height: 42.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 2.0, height: 42.0)), cornerRadius: 1.0)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})
|
||||
|
||||
self.leftHandleView.image = handleImage
|
||||
self.leftHandleView.isUserInteractionEnabled = true
|
||||
self.leftHandleView.tintColor = .white
|
||||
@ -127,6 +150,9 @@ final class VideoScrubberComponent: Component {
|
||||
self.rightHandleView.isUserInteractionEnabled = true
|
||||
self.rightHandleView.tintColor = .white
|
||||
|
||||
self.cursorView.image = positionImage
|
||||
self.cursorView.isUserInteractionEnabled = true
|
||||
|
||||
self.borderView.image = generateImage(CGSize(width: 1.0, height: scrubberHeight), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
@ -151,7 +177,7 @@ final class VideoScrubberComponent: Component {
|
||||
|
||||
self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:))))
|
||||
self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:))))
|
||||
//self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:))))
|
||||
self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -234,6 +260,13 @@ final class VideoScrubberComponent: Component {
|
||||
}
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
|
||||
@objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let _ = self.component else {
|
||||
return
|
||||
}
|
||||
//let location = gestureRecognizer.location(in: self)
|
||||
}
|
||||
|
||||
func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let previousFramesUpdateTimestamp = self.component?.framesUpdateTimestamp
|
||||
@ -245,15 +278,15 @@ final class VideoScrubberComponent: Component {
|
||||
|
||||
if component.framesUpdateTimestamp != previousFramesUpdateTimestamp {
|
||||
for i in 0 ..< component.frames.count {
|
||||
let transparentFrameLayer: CALayer
|
||||
let opaqueFrameLayer: CALayer
|
||||
let transparentFrameLayer: VideoFrameLayer
|
||||
let opaqueFrameLayer: VideoFrameLayer
|
||||
if i >= self.transparentFrameLayers.count {
|
||||
transparentFrameLayer = SimpleLayer()
|
||||
transparentFrameLayer = VideoFrameLayer()
|
||||
transparentFrameLayer.masksToBounds = true
|
||||
transparentFrameLayer.contentsGravity = .resizeAspectFill
|
||||
self.transparentFramesContainer.layer.addSublayer(transparentFrameLayer)
|
||||
self.transparentFrameLayers.append(transparentFrameLayer)
|
||||
opaqueFrameLayer = SimpleLayer()
|
||||
opaqueFrameLayer = VideoFrameLayer()
|
||||
opaqueFrameLayer.masksToBounds = true
|
||||
opaqueFrameLayer.contentsGravity = .resizeAspectFill
|
||||
self.opaqueFramesContainer.layer.addSublayer(opaqueFrameLayer)
|
||||
|
@ -191,41 +191,41 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
accountSettingsController.parentController = self
|
||||
controllers.append(accountSettingsController)
|
||||
|
||||
tabBarController.cameraItemAndAction = (
|
||||
UITabBarItem(title: "Camera", image: UIImage(bundleImageName: "Chat List/Tabs/IconCamera"), tag: 2),
|
||||
{ [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let coordinator = self.openStoryCamera(
|
||||
transitionIn: nil,
|
||||
transitionedIn: { [weak self] in
|
||||
guard let self, let rootTabController = self.rootTabController else {
|
||||
return
|
||||
}
|
||||
if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
|
||||
rootTabController.selectedIndex = index
|
||||
}
|
||||
},
|
||||
transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if finished {
|
||||
if let chatListController = self.chatListController as? ChatListControllerImpl, let transitionView = chatListController.transitionViewForOwnStoryItem() {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
)
|
||||
// tabBarController.cameraItemAndAction = (
|
||||
// UITabBarItem(title: "Camera", image: UIImage(bundleImageName: "Chat List/Tabs/IconCamera"), tag: 2),
|
||||
// { [weak self] in
|
||||
// guard let self else {
|
||||
// return
|
||||
// }
|
||||
// let coordinator = self.openStoryCamera(
|
||||
// transitionIn: nil,
|
||||
// transitionedIn: { [weak self] in
|
||||
// guard let self, let rootTabController = self.rootTabController else {
|
||||
// return
|
||||
// }
|
||||
// if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController}) {
|
||||
// rootTabController.selectedIndex = index
|
||||
// }
|
||||
// },
|
||||
// transitionOut: { [weak self] finished in
|
||||
// guard let self else {
|
||||
// return nil
|
||||
// }
|
||||
// if finished {
|
||||
// if let chatListController = self.chatListController as? ChatListControllerImpl, let transitionView = chatListController.transitionViewForOwnStoryItem() {
|
||||
// return StoryCameraTransitionOut(
|
||||
// destinationView: transitionView,
|
||||
// destinationRect: transitionView.bounds,
|
||||
// destinationCornerRadius: transitionView.bounds.height / 2.0
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
// )
|
||||
// coordinator?.animateIn()
|
||||
// }
|
||||
// )
|
||||
|
||||
tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : (controllers.count - 2))
|
||||
|
||||
@ -400,16 +400,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
let imageFlags: TelegramMediaImageFlags = []
|
||||
// var stickerFiles: [TelegramMediaFile] = []
|
||||
// if !stickers.isEmpty {
|
||||
// for fileReference in stickers {
|
||||
// stickerFiles.append(fileReference.media)
|
||||
// }
|
||||
// }
|
||||
// if !stickerFiles.isEmpty {
|
||||
// attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles))
|
||||
// imageFlags.insert(.hasStickers)
|
||||
// }
|
||||
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags)
|
||||
if let timeout, timeout > 0 && timeout <= 60 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user