mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Camera and editor improvements
This commit is contained in:
parent
3df2d3cad5
commit
ab69b9e982
@ -77,7 +77,7 @@ private final class CameraContext {
|
||||
}
|
||||
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if timestamp > self.lastSnapshotTimestamp + 5.0 {
|
||||
if timestamp > self.lastSnapshotTimestamp + 2.5 {
|
||||
self.savePreviewSnapshot(pixelBuffer: pixelBuffer)
|
||||
self.lastSnapshotTimestamp = timestamp
|
||||
}
|
||||
@ -306,19 +306,25 @@ public final class Camera {
|
||||
}
|
||||
|
||||
public func startCapture() {
|
||||
#if targetEnvironment(simulator)
|
||||
#else
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.startCapture()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func stopCapture(invalidate: Bool = false) {
|
||||
#if targetEnvironment(simulator)
|
||||
#else
|
||||
self.queue.async {
|
||||
if let context = self.contextRef?.takeUnretainedValue() {
|
||||
context.stopCapture(invalidate: invalidate)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func togglePosition() {
|
||||
|
@ -63,7 +63,7 @@ public class CameraSimplePreviewView: UIView {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.placeholderView.frame = self.bounds
|
||||
self.placeholderView.frame = self.bounds.insetBy(dx: -1.0, dy: -1.0)
|
||||
}
|
||||
|
||||
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
||||
|
@ -2542,7 +2542,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.fullScreenEffectView = nil
|
||||
fullScreenEffectView.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
||||
if let value = RippleEffectView(centerLocation: centerLocation, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -2559,6 +2559,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var storyUploadProgress: Float?
|
||||
public func updateStoryUploadProgress(_ progress: Float?) {
|
||||
self.storyUploadProgress = progress.flatMap { max(0.027, min(0.99, $0)) }
|
||||
self.chatListDisplayNode.requestNavigationBarLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
|
||||
public func scrollToStories() {
|
||||
|
||||
}
|
||||
|
||||
private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var tabContainerOffset: CGFloat = 0.0
|
||||
if !self.displayNavigationBar {
|
||||
@ -5463,15 +5473,17 @@ private final class ChatListLocationContext {
|
||||
self.proxyButton = nil
|
||||
}
|
||||
|
||||
self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .icon(imageName: "Chat List/AddStoryIcon"),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self, let parentController = self.parentController else {
|
||||
return
|
||||
if case .chatList(.root) = self.location {
|
||||
self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .icon(imageName: "Chat List/AddStoryIcon"),
|
||||
pressed: { [weak self] _ in
|
||||
guard let self, let parentController = self.parentController else {
|
||||
return
|
||||
}
|
||||
parentController.openStoryCamera()
|
||||
}
|
||||
parentController.openStoryCamera()
|
||||
}
|
||||
)))
|
||||
)))
|
||||
}
|
||||
|
||||
self.chatListTitle = titleContent
|
||||
|
||||
|
@ -1854,6 +1854,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
secondaryContent: headerContent?.secondaryContent,
|
||||
secondaryTransition: self.inlineStackContainerTransitionFraction,
|
||||
storySubscriptions: self.controller?.storySubscriptions,
|
||||
uploadProgress: self.controller?.storyUploadProgress,
|
||||
tabsNode: tabsNode,
|
||||
activateSearch: { [weak self] searchContentNode in
|
||||
guard let self, let controller = self.controller else {
|
||||
@ -1934,6 +1935,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func requestNavigationBarLayout(transition: ContainedViewLayoutTransition) {
|
||||
guard let (layout, _, _, _, _) = self.containerLayout else {
|
||||
return
|
||||
}
|
||||
let _ = self.updateNavigationBar(layout: layout, transition: transition)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
var navigationBarHeight = navigationBarHeight
|
||||
var visualNavigationHeight = visualNavigationHeight
|
||||
|
@ -825,6 +825,52 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
|
||||
public func setShapeLayerStrokeStart(layer: CAShapeLayer, strokeStart: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.strokeStart = strokeStart
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
let previousStrokeStart = layer.strokeStart
|
||||
layer.strokeStart = strokeStart
|
||||
|
||||
layer.animate(
|
||||
from: previousStrokeStart as NSNumber,
|
||||
to: strokeStart as NSNumber,
|
||||
keyPath: "strokeStart",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: false,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setShapeLayerStrokeEnd(layer: CAShapeLayer, strokeEnd: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.strokeEnd = strokeEnd
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
let previousStrokeEnd = layer.strokeEnd
|
||||
layer.strokeEnd = strokeEnd
|
||||
|
||||
layer.animate(
|
||||
from: previousStrokeEnd as NSNumber,
|
||||
to: strokeEnd as NSNumber,
|
||||
keyPath: "strokeEnd",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: false,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func setShapeLayerFillColor(layer: CAShapeLayer, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||
if let current = layer.layerTintColor, current == color.cgColor {
|
||||
completion?(true)
|
||||
|
@ -58,7 +58,7 @@ public final class RoundedRectangle: Component {
|
||||
}
|
||||
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
|
||||
UIGraphicsEndImageContext()
|
||||
} else if component.colors.count > 1{
|
||||
} else if component.colors.count > 1 {
|
||||
let imageSize = availableSize
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
|
@ -42,6 +42,7 @@ public struct Font {
|
||||
|
||||
public enum Weight {
|
||||
case regular
|
||||
case thin
|
||||
case light
|
||||
case medium
|
||||
case semibold
|
||||
@ -59,6 +60,8 @@ public struct Font {
|
||||
|
||||
var weight: UIFont.Weight {
|
||||
switch self {
|
||||
case .thin:
|
||||
return .thin
|
||||
case .light:
|
||||
return .light
|
||||
case .medium:
|
||||
@ -78,6 +81,8 @@ public struct Font {
|
||||
switch self {
|
||||
case .regular:
|
||||
return "regular"
|
||||
case .thin:
|
||||
return "thin"
|
||||
case .light:
|
||||
return "light"
|
||||
case .medium:
|
||||
|
@ -176,21 +176,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToAngle(snappedAngle)
|
||||
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
|
||||
if let self, let entityView = self.entityView {
|
||||
entityView.onSnapUpdated(type, snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,11 +218,13 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
@ -283,7 +273,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
@ -292,10 +282,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
case .ended, .cancelled:
|
||||
self.snapTool.reset()
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -310,11 +299,16 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
if case .began = gestureRecognizer.state {
|
||||
entityView.onInteractionUpdated(true)
|
||||
}
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
case .ended, .cancelled:
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -332,12 +326,14 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
rotation = gestureRecognizer.rotation
|
||||
updatedRotation += rotation
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
case .ended, .cancelled:
|
||||
entityView.onInteractionUpdated(false)
|
||||
self.snapTool.rotationReset()
|
||||
default:
|
||||
break
|
||||
|
@ -56,6 +56,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
public private(set) var selectedEntityView: DrawingEntityView?
|
||||
|
||||
public var getEntityEdgePositions: () -> UIEdgeInsets? = { return nil }
|
||||
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
|
||||
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
|
||||
public var getEntityAdditionalScale: () -> CGFloat = { return 1.0 }
|
||||
@ -66,10 +67,18 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
|
||||
var entityAdded: (DrawingEntity) -> Void = { _ in }
|
||||
var entityRemoved: (DrawingEntity) -> Void = { _ in }
|
||||
|
||||
|
||||
private let topEdgeView = UIView()
|
||||
private let leftEdgeView = UIView()
|
||||
private let rightEdgeView = UIView()
|
||||
private let bottomEdgeView = UIView()
|
||||
private let xAxisView = UIView()
|
||||
private let yAxisView = UIView()
|
||||
private let angleLayer = SimpleShapeLayer()
|
||||
|
||||
public var onInteractionUpdated: (Bool) -> Void = { _ in }
|
||||
public var edgePreviewUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
public init(context: AccountContext, size: CGSize) {
|
||||
@ -82,6 +91,22 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
self.tapGestureRecognizer = tapGestureRecognizer
|
||||
|
||||
self.topEdgeView.alpha = 0.0
|
||||
self.topEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.topEdgeView.isUserInteractionEnabled = false
|
||||
|
||||
self.leftEdgeView.alpha = 0.0
|
||||
self.leftEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.leftEdgeView.isUserInteractionEnabled = false
|
||||
|
||||
self.rightEdgeView.alpha = 0.0
|
||||
self.rightEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.rightEdgeView.isUserInteractionEnabled = false
|
||||
|
||||
self.bottomEdgeView.alpha = 0.0
|
||||
self.bottomEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.bottomEdgeView.isUserInteractionEnabled = false
|
||||
|
||||
self.xAxisView.alpha = 0.0
|
||||
self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||
self.xAxisView.isUserInteractionEnabled = false
|
||||
@ -94,6 +119,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.angleLayer.opacity = 0.0
|
||||
self.angleLayer.lineDashPattern = [12, 12] as [NSNumber]
|
||||
|
||||
self.addSubview(self.topEdgeView)
|
||||
self.addSubview(self.leftEdgeView)
|
||||
self.addSubview(self.rightEdgeView)
|
||||
self.addSubview(self.bottomEdgeView)
|
||||
|
||||
self.addSubview(self.xAxisView)
|
||||
self.addSubview(self.yAxisView)
|
||||
self.layer.addSublayer(self.angleLayer)
|
||||
@ -103,28 +133,41 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let referenceSize = self.convert(CGRect(origin: .zero, size: CGSize(width: 1.0 + UIScreenPixel, height: 1.0)), from: nil)
|
||||
let width = ceil(referenceSize.width)
|
||||
|
||||
if let edges = self.getEntityEdgePositions() {
|
||||
self.topEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
|
||||
self.topEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.top)
|
||||
|
||||
self.bottomEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
|
||||
self.bottomEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.bottom)
|
||||
|
||||
self.leftEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
|
||||
self.leftEdgeView.center = CGPoint(x: edges.left, y: self.bounds.height / 2.0)
|
||||
|
||||
self.rightEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
|
||||
self.rightEdgeView.center = CGPoint(x: edges.right, y: self.bounds.height / 2.0)
|
||||
}
|
||||
|
||||
let point = self.getEntityCenterPosition()
|
||||
self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 6.0, height: 3000.0))
|
||||
self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0))
|
||||
self.xAxisView.center = point
|
||||
self.xAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
|
||||
|
||||
self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0))
|
||||
self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
|
||||
self.yAxisView.center = point
|
||||
self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
|
||||
|
||||
let anglePath = CGMutablePath()
|
||||
anglePath.move(to: CGPoint(x: 0.0, y: 3.0))
|
||||
anglePath.addLine(to: CGPoint(x: 3000.0, y: 3.0))
|
||||
anglePath.move(to: CGPoint(x: 0.0, y: width / 2.0))
|
||||
anglePath.addLine(to: CGPoint(x: 3000.0, y: width / 2.0))
|
||||
self.angleLayer.path = anglePath
|
||||
self.angleLayer.lineWidth = 6.0
|
||||
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0))
|
||||
self.angleLayer.lineWidth = width
|
||||
self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width))
|
||||
}
|
||||
|
||||
public var entities: [DrawingEntity] {
|
||||
@ -286,57 +329,63 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
view.containerView = self
|
||||
|
||||
view.onSnapToXAxis = { [weak self, weak view] snappedToX in
|
||||
guard let strongSelf = self, let strongView = view else {
|
||||
return
|
||||
}
|
||||
func processSnap(snapped: Bool, snapView: UIView) {
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if snappedToX {
|
||||
strongSelf.insertSubview(strongSelf.xAxisView, belowSubview: strongView)
|
||||
if strongSelf.xAxisView.alpha < 1.0 {
|
||||
strongSelf.hapticFeedback.impact(.light)
|
||||
if snapped {
|
||||
self.insertSubview(snapView, belowSubview: view)
|
||||
if snapView.alpha < 1.0 {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 1.0)
|
||||
transition.updateAlpha(layer: snapView.layer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 0.0)
|
||||
transition.updateAlpha(layer: snapView.layer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
view.onSnapToYAxis = { [weak self, weak view] snappedToY in
|
||||
guard let strongSelf = self, let strongView = view else {
|
||||
|
||||
view.onSnapUpdated = { [weak self, weak view] type, snapped in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if snappedToY {
|
||||
strongSelf.insertSubview(strongSelf.yAxisView, belowSubview: strongView)
|
||||
if strongSelf.yAxisView.alpha < 1.0 {
|
||||
strongSelf.hapticFeedback.impact(.light)
|
||||
switch type {
|
||||
case .centerX:
|
||||
processSnap(snapped: snapped, snapView: self.xAxisView)
|
||||
case .centerY:
|
||||
processSnap(snapped: snapped, snapView: self.yAxisView)
|
||||
case .top:
|
||||
processSnap(snapped: snapped, snapView: self.topEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .left:
|
||||
processSnap(snapped: snapped, snapView: self.leftEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .right:
|
||||
processSnap(snapped: snapped, snapView: self.rightEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case .bottom:
|
||||
processSnap(snapped: snapped, snapView: self.bottomEdgeView)
|
||||
self.edgePreviewUpdated(snapped)
|
||||
case let .rotation(angle):
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if let angle, let view {
|
||||
self.layer.insertSublayer(self.angleLayer, below: view.layer)
|
||||
self.angleLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
|
||||
if self.angleLayer.opacity < 1.0 {
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
transition.updateAlpha(layer: self.angleLayer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: self.angleLayer, alpha: 0.0)
|
||||
}
|
||||
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
view.onSnapToAngle = { [weak self, weak view] snappedToAngle in
|
||||
guard let strongSelf = self, let strongView = view else {
|
||||
return
|
||||
}
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
if let snappedToAngle {
|
||||
strongSelf.layer.insertSublayer(strongSelf.angleLayer, below: strongView.layer)
|
||||
strongSelf.angleLayer.transform = CATransform3DMakeRotation(snappedToAngle, 0.0, 0.0, 1.0)
|
||||
if strongSelf.angleLayer.opacity < 1.0 {
|
||||
strongSelf.hapticFeedback.impact(.light)
|
||||
}
|
||||
transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
view.onPositionUpdated = { [weak self] position in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
if let self {
|
||||
self.angleLayer.position = position
|
||||
}
|
||||
}
|
||||
view.onInteractionUpdated = { [weak self] interacting in
|
||||
if let self {
|
||||
self.onInteractionUpdated(interacting)
|
||||
}
|
||||
strongSelf.angleLayer.position = position
|
||||
}
|
||||
|
||||
view.update()
|
||||
@ -603,10 +652,9 @@ public class DrawingEntityView: UIView {
|
||||
public weak var selectionView: DrawingEntitySelectionView?
|
||||
weak var containerView: DrawingEntitiesView?
|
||||
|
||||
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||
var onSnapToYAxis: (Bool) -> Void = { _ in }
|
||||
var onSnapToAngle: (CGFloat?) -> Void = { _ in }
|
||||
var onSnapUpdated: (DrawingEntitySnapTool.SnapType, Bool) -> Void = { _, _ in }
|
||||
var onPositionUpdated: (CGPoint) -> Void = { _ in }
|
||||
var onInteractionUpdated: (Bool) -> Void = { _ in }
|
||||
|
||||
init(context: AccountContext, entity: DrawingEntity) {
|
||||
self.context = context
|
||||
|
@ -145,20 +145,26 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
}
|
||||
}
|
||||
|
||||
private var beganRotating = false
|
||||
@objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
|
||||
var updatedRotation = self.mediaEntity.rotation
|
||||
var rotation: CGFloat = 0.0
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
break
|
||||
self.beganRotating = false
|
||||
case .changed:
|
||||
rotation = gestureRecognizer.rotation
|
||||
updatedRotation += rotation
|
||||
|
||||
gestureRecognizer.rotation = 0.0
|
||||
if self.beganRotating || abs(rotation) >= 0.08 * .pi || abs(self.mediaEntity.rotation) >= 0.03 {
|
||||
if !self.beganRotating {
|
||||
self.beganRotating = true
|
||||
} else {
|
||||
updatedRotation += rotation
|
||||
}
|
||||
gestureRecognizer.rotation = 0.0
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
break
|
||||
self.beganRotating = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -1137,10 +1137,18 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
presentColorPicker(state.currentColor)
|
||||
}
|
||||
}
|
||||
|
||||
var controlsVisible = true
|
||||
if state.drawingViewState.isDrawing {
|
||||
controlsVisible = false
|
||||
}
|
||||
|
||||
let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778))
|
||||
let previewTopInset: CGFloat = floorToScreenPixels(context.availableSize.height - previewSize.height) / 2.0
|
||||
|
||||
var topInset = environment.safeInsets.top + 31.0
|
||||
if component.sourceHint == .storyEditor {
|
||||
topInset += 75.0
|
||||
topInset = previewTopInset + 31.0
|
||||
}
|
||||
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0
|
||||
|
||||
@ -1178,6 +1186,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
)
|
||||
context.add(bottomGradient
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomGradient.size.height / 2.0))
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
if let textEntity = state.selectedEntity as? DrawingTextEntity {
|
||||
@ -1318,6 +1327,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1345,6 +1355,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1372,6 +1383,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1399,6 +1411,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1426,6 +1439,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
delay += 0.025
|
||||
@ -1454,6 +1468,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1481,6 +1496,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
offsetX += delta
|
||||
|
||||
@ -1508,13 +1524,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
})
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
if state.selectedEntity is DrawingStickerEntity || state.selectedEntity is DrawingTextEntity {
|
||||
} else {
|
||||
let tools = tools.update(
|
||||
component: ToolsComponent(
|
||||
state: component.isVideo ? state.drawingState.forVideo() : state.drawingState,
|
||||
state: component.isVideo || component.sourceHint == .storyEditor ? state.drawingState.forVideo() : state.drawingState,
|
||||
isFocused: false,
|
||||
tag: toolsTag,
|
||||
toolPressed: { [weak state] tool in
|
||||
@ -1551,6 +1568,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
completion()
|
||||
}
|
||||
}))
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
}
|
||||
|
||||
@ -1725,7 +1743,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
)
|
||||
context.add(textSize
|
||||
.position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
|
||||
.opacity(sizeSliderVisible ? 1.0 : 0.0)
|
||||
.opacity(sizeSliderVisible && controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let undoButton = undoButton.update(
|
||||
@ -1745,7 +1763,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
context.add(undoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
|
||||
.scale(isEditingText ? 0.01 : 1.0)
|
||||
.opacity(isEditingText ? 0.0 : 1.0)
|
||||
.opacity(isEditingText || !controlsVisible ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
|
||||
@ -1765,7 +1783,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||
.opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0)
|
||||
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let clearAllButton = clearAllButton.update(
|
||||
@ -1785,9 +1803,16 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
context.add(clearAllButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset))
|
||||
.scale(isEditingText ? 0.01 : 1.0)
|
||||
.opacity(isEditingText ? 0.0 : 1.0)
|
||||
.opacity(isEditingText || !controlsVisible ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
let textButtonTopInset: CGFloat
|
||||
if let sourceHint = component.sourceHint, case .storyEditor = sourceHint {
|
||||
textButtonTopInset = environment.statusBarHeight
|
||||
} else {
|
||||
textButtonTopInset = topInset
|
||||
}
|
||||
|
||||
let textCancelButton = textCancelButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
@ -1823,7 +1848,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(textDoneButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset))
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: textButtonTopInset))
|
||||
.scale(isEditingText ? 1.0 : 0.01)
|
||||
.opacity(isEditingText ? 1.0 : 0.0)
|
||||
)
|
||||
@ -1870,6 +1895,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let modeRightInset: CGFloat = 57.0
|
||||
@ -1919,6 +1945,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.cornerRadius(12.0)
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let doneButton = doneButton.update(
|
||||
@ -1938,7 +1965,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
doneButtonPosition = doneButtonPosition.offsetBy(dx: -2.0, dy: 0.0)
|
||||
doneButtonPosition.x = doneButtonPosition.x - 2.0
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + doneButton.size.height / 2.0)
|
||||
}
|
||||
context.add(doneButton
|
||||
.position(doneButtonPosition)
|
||||
@ -1955,6 +1983,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
})
|
||||
transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true)
|
||||
})
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let selectedIndex: Int
|
||||
@ -2013,9 +2042,13 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
let modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
|
||||
var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
|
||||
if component.sourceHint == .storyEditor {
|
||||
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 8.0 + modeAndSize.size.height / 2.0)
|
||||
}
|
||||
context.add(modeAndSize
|
||||
.position(modeAndSizePosition)
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
var animatingOut = false
|
||||
@ -2049,10 +2082,12 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
)
|
||||
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
backButtonPosition = backButtonPosition.offsetBy(dx: 2.0, dy: 0.0)
|
||||
backButtonPosition.x = backButtonPosition.x + 2.0
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + backButton.size.height / 2.0)
|
||||
}
|
||||
context.add(backButton
|
||||
.position(backButtonPosition)
|
||||
.opacity(controlsVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
@ -3019,6 +3054,23 @@ public final class DrawingToolsInteraction {
|
||||
}
|
||||
}
|
||||
|
||||
public func endTextEditing(reset: Bool) {
|
||||
if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView {
|
||||
entityView.endEditing(reset: reset)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateEntitySize(_ size: CGFloat) {
|
||||
if let selectedEntityView = self.entitiesView.selectedEntityView {
|
||||
if let textEntity = selectedEntityView.entity as? DrawingTextEntity {
|
||||
textEntity.fontSize = size
|
||||
} else {
|
||||
selectedEntityView.entity.lineWidth = size
|
||||
}
|
||||
selectedEntityView.update()
|
||||
}
|
||||
}
|
||||
|
||||
func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) {
|
||||
// self.entitiesView.pause()
|
||||
//
|
||||
|
@ -185,21 +185,9 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToAngle(snappedAngle)
|
||||
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
|
||||
if let self, let entityView = self.entityView {
|
||||
entityView.onSnapUpdated(type, snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -240,11 +228,13 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
@ -337,7 +327,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
@ -345,10 +335,9 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
case .ended, .cancelled:
|
||||
self.snapTool.reset()
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -363,11 +352,16 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
if case .began = gestureRecognizer.state {
|
||||
entityView.onInteractionUpdated(true)
|
||||
}
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
case .ended, .cancelled:
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -385,6 +379,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
rotation = gestureRecognizer.rotation
|
||||
updatedRotation += rotation
|
||||
@ -392,6 +387,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
gestureRecognizer.rotation = 0.0
|
||||
case .ended, .cancelled:
|
||||
self.snapTool.rotationReset()
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -310,21 +310,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToAngle(snappedAngle)
|
||||
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
|
||||
if let self, let entityView = self.entityView {
|
||||
entityView.onSnapUpdated(type, snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -365,11 +353,13 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
@ -399,7 +389,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
|
||||
}
|
||||
|
||||
entity.position = updatedPosition
|
||||
@ -413,6 +403,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
if self.currentHandle != nil {
|
||||
self.snapTool.rotationReset()
|
||||
}
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -427,11 +418,16 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
if case .began = gestureRecognizer.state {
|
||||
entityView.onInteractionUpdated(true)
|
||||
}
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.scale = entity.scale * scale
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
case .cancelled, .ended:
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -449,6 +445,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
rotation = gestureRecognizer.rotation
|
||||
updatedRotation += rotation
|
||||
@ -456,6 +453,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
gestureRecognizer.rotation = 0.0
|
||||
case .ended, .cancelled:
|
||||
self.snapTool.rotationReset()
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -509,32 +507,77 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
private let snapTimeout = 1.0
|
||||
|
||||
class DrawingEntitySnapTool {
|
||||
private var xState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||
private var yState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||
enum SnapType {
|
||||
case centerX
|
||||
case centerY
|
||||
case top
|
||||
case left
|
||||
case right
|
||||
case bottom
|
||||
case rotation(CGFloat?)
|
||||
|
||||
static var allPositionTypes: [SnapType] {
|
||||
return [
|
||||
.centerX,
|
||||
.centerY,
|
||||
.top,
|
||||
.left,
|
||||
.right,
|
||||
.bottom
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
struct SnapState {
|
||||
let skipped: CGFloat
|
||||
let waitForLeave: Bool
|
||||
}
|
||||
|
||||
private var topEdgeState: SnapState?
|
||||
private var leftEdgeState: SnapState?
|
||||
private var rightEdgeState: SnapState?
|
||||
private var bottomEdgeState: SnapState?
|
||||
|
||||
private var xState: SnapState?
|
||||
private var yState: SnapState?
|
||||
|
||||
private var rotationState: (angle: CGFloat, skipped: CGFloat, waitForLeave: Bool)?
|
||||
|
||||
var onSnapXUpdated: (Bool) -> Void = { _ in }
|
||||
var onSnapYUpdated: (Bool) -> Void = { _ in }
|
||||
var onSnapRotationUpdated: (CGFloat?) -> Void = { _ in }
|
||||
var onSnapUpdated: (SnapType, Bool) -> Void = { _, _ in }
|
||||
|
||||
var previousTopEdgeSnapTimestamp: Double?
|
||||
var previousLeftEdgeSnapTimestamp: Double?
|
||||
var previousRightEdgeSnapTimestamp: Double?
|
||||
var previousBottomEdgeSnapTimestamp: Double?
|
||||
|
||||
var previousXSnapTimestamp: Double?
|
||||
var previousYSnapTimestamp: Double?
|
||||
var previousRotationSnapTimestamp: Double?
|
||||
|
||||
func reset() {
|
||||
self.topEdgeState = nil
|
||||
self.leftEdgeState = nil
|
||||
self.rightEdgeState = nil
|
||||
self.bottomEdgeState = nil
|
||||
self.xState = nil
|
||||
self.yState = nil
|
||||
|
||||
self.onSnapXUpdated(false)
|
||||
self.onSnapYUpdated(false)
|
||||
for type in SnapType.allPositionTypes {
|
||||
self.onSnapUpdated(type, false)
|
||||
}
|
||||
}
|
||||
|
||||
func rotationReset() {
|
||||
self.rotationState = nil
|
||||
self.onSnapRotationUpdated(nil)
|
||||
self.onSnapUpdated(.rotation(nil), false)
|
||||
}
|
||||
|
||||
func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) {
|
||||
self.topEdgeState = nil
|
||||
self.leftEdgeState = nil
|
||||
self.rightEdgeState = nil
|
||||
self.bottomEdgeState = nil
|
||||
|
||||
self.xState = nil
|
||||
self.yState = nil
|
||||
|
||||
@ -543,94 +586,222 @@ class DrawingEntitySnapTool {
|
||||
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta {
|
||||
self.xState = (0.0, true)
|
||||
self.xState = SnapState(skipped: 0.0, waitForLeave: true)
|
||||
}
|
||||
|
||||
if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta {
|
||||
self.yState = (0.0, true)
|
||||
self.yState = SnapState(skipped: 0.0, waitForLeave: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint {
|
||||
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint, size: CGSize) -> CGPoint {
|
||||
var updatedPosition = updatedPosition
|
||||
|
||||
guard let snapCenterLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() else {
|
||||
return updatedPosition
|
||||
}
|
||||
let snapEdgeLocations = (entityView.superview as? DrawingEntitiesView)?.getEntityEdgePositions()
|
||||
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
|
||||
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapXVelocity: CGFloat = snapXDelta * 12.0
|
||||
let snapXSkipTranslation: CGFloat = snapXDelta * 2.0
|
||||
let snapDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapVelocity: CGFloat = snapDelta * 12.0
|
||||
let snapSkipTranslation: CGFloat = snapDelta * 2.0
|
||||
|
||||
if abs(velocity.x) < snapXVelocity || self.xState?.waitForLeave == true {
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if let (skipped, waitForLeave) = self.xState {
|
||||
if waitForLeave {
|
||||
if updatedPosition.x > snapLocation.x - snapXDelta * 2.0 && updatedPosition.x < snapLocation.x + snapXDelta * 2.0 {
|
||||
|
||||
let topPoint = updatedPosition.y - size.height / 2.0
|
||||
let leftPoint = updatedPosition.x - size.width / 2.0
|
||||
let rightPoint = updatedPosition.x + size.width / 2.0
|
||||
let bottomPoint = updatedPosition.y + size.height / 2.0
|
||||
|
||||
func process(
|
||||
state: SnapState?,
|
||||
velocity: CGFloat,
|
||||
delta: CGFloat,
|
||||
value: CGFloat,
|
||||
snapVelocity: CGFloat,
|
||||
snapToValue: CGFloat?,
|
||||
snapDelta: CGFloat,
|
||||
snapSkipTranslation: CGFloat,
|
||||
previousSnapTimestamp: Double?,
|
||||
onSnapUpdated: (Bool) -> Void
|
||||
) -> (
|
||||
value: CGFloat,
|
||||
state: SnapState?,
|
||||
snapTimestamp: Double?
|
||||
) {
|
||||
var updatedValue = value
|
||||
var updatedState = state
|
||||
var updatedPreviousSnapTimestamp = previousSnapTimestamp
|
||||
if abs(velocity) < snapVelocity || state?.waitForLeave == true {
|
||||
if let snapToValue {
|
||||
if let state {
|
||||
let skipped = state.skipped
|
||||
let waitForLeave = state.waitForLeave
|
||||
if waitForLeave {
|
||||
if value > snapToValue - snapDelta * 2.0 && value < snapToValue + snapDelta * 2.0 {
|
||||
|
||||
} else {
|
||||
updatedState = nil
|
||||
}
|
||||
} else if abs(skipped) < snapSkipTranslation {
|
||||
updatedState = SnapState(skipped: skipped + delta, waitForLeave: false)
|
||||
updatedValue = snapToValue
|
||||
} else {
|
||||
self.xState = nil
|
||||
updatedState = SnapState(skipped: snapSkipTranslation, waitForLeave: true)
|
||||
onSnapUpdated(false)
|
||||
}
|
||||
} else if abs(skipped) < snapXSkipTranslation {
|
||||
self.xState = (skipped + delta.x, false)
|
||||
updatedPosition.x = snapLocation.x
|
||||
} else {
|
||||
self.xState = (snapXSkipTranslation, true)
|
||||
self.onSnapXUpdated(false)
|
||||
}
|
||||
} else {
|
||||
if updatedPosition.x > snapLocation.x - snapXDelta && updatedPosition.x < snapLocation.x + snapXDelta {
|
||||
if let previousXSnapTimestamp, currentTimestamp - previousXSnapTimestamp < snapTimeout {
|
||||
|
||||
} else {
|
||||
self.previousXSnapTimestamp = currentTimestamp
|
||||
self.xState = (0.0, false)
|
||||
updatedPosition.x = snapLocation.x
|
||||
self.onSnapXUpdated(true)
|
||||
if value > snapToValue - snapDelta && value < snapToValue + snapDelta {
|
||||
if let previousSnapTimestamp, currentTimestamp - previousSnapTimestamp < snapTimeout {
|
||||
|
||||
} else {
|
||||
updatedPreviousSnapTimestamp = currentTimestamp
|
||||
updatedState = SnapState(skipped: 0.0, waitForLeave: false)
|
||||
updatedValue = snapToValue
|
||||
onSnapUpdated(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedState = nil
|
||||
onSnapUpdated(false)
|
||||
}
|
||||
} else {
|
||||
self.xState = nil
|
||||
self.onSnapXUpdated(false)
|
||||
return (updatedValue, updatedState, updatedPreviousSnapTimestamp)
|
||||
}
|
||||
|
||||
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||
let snapYVelocity: CGFloat = snapYDelta * 12.0
|
||||
let snapYSkipTranslation: CGFloat = snapYDelta * 2.0
|
||||
let (updatedXValue, updatedXState, updatedXPreviousTimestamp) = process(
|
||||
state: self.xState,
|
||||
velocity: velocity.x,
|
||||
delta: delta.x,
|
||||
value: updatedPosition.x,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapCenterLocation.x,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousXSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.centerX, snapped)
|
||||
}
|
||||
)
|
||||
self.xState = updatedXState
|
||||
self.previousXSnapTimestamp = updatedXPreviousTimestamp
|
||||
|
||||
if abs(velocity.y) < snapYVelocity || self.yState?.waitForLeave == true {
|
||||
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||
if let (skipped, waitForLeave) = self.yState {
|
||||
if waitForLeave {
|
||||
if updatedPosition.y > snapLocation.y - snapYDelta * 2.0 && updatedPosition.y < snapLocation.y + snapYDelta * 2.0 {
|
||||
|
||||
} else {
|
||||
self.yState = nil
|
||||
}
|
||||
} else if abs(skipped) < snapYSkipTranslation {
|
||||
self.yState = (skipped + delta.y, false)
|
||||
updatedPosition.y = snapLocation.y
|
||||
} else {
|
||||
self.yState = (snapYSkipTranslation, true)
|
||||
self.onSnapYUpdated(false)
|
||||
let (updatedYValue, updatedYState, updatedYPreviousTimestamp) = process(
|
||||
state: self.yState,
|
||||
velocity: velocity.y,
|
||||
delta: delta.y,
|
||||
value: updatedPosition.y,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapCenterLocation.y,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousYSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.centerY, snapped)
|
||||
}
|
||||
)
|
||||
self.yState = updatedYState
|
||||
self.previousYSnapTimestamp = updatedYPreviousTimestamp
|
||||
|
||||
if let snapEdgeLocations {
|
||||
if updatedXState == nil {
|
||||
let (updatedXLeftEdgeValue, updatedLeftEdgeState, updatedLeftEdgePreviousTimestamp) = process(
|
||||
state: self.leftEdgeState,
|
||||
velocity: velocity.x,
|
||||
delta: delta.x,
|
||||
value: leftPoint,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapEdgeLocations.left,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousLeftEdgeSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.left, snapped)
|
||||
}
|
||||
)
|
||||
self.leftEdgeState = updatedLeftEdgeState
|
||||
self.previousLeftEdgeSnapTimestamp = updatedLeftEdgePreviousTimestamp
|
||||
|
||||
if updatedLeftEdgeState != nil {
|
||||
updatedPosition.x = updatedXLeftEdgeValue + size.width / 2.0
|
||||
|
||||
self.rightEdgeState = nil
|
||||
self.previousRightEdgeSnapTimestamp = nil
|
||||
} else {
|
||||
if updatedPosition.y > snapLocation.y - snapYDelta && updatedPosition.y < snapLocation.y + snapYDelta {
|
||||
if let previousYSnapTimestamp, currentTimestamp - previousYSnapTimestamp < snapTimeout {
|
||||
|
||||
} else {
|
||||
self.previousYSnapTimestamp = currentTimestamp
|
||||
self.yState = (0.0, false)
|
||||
updatedPosition.y = snapLocation.y
|
||||
self.onSnapYUpdated(true)
|
||||
let (updatedXRightEdgeValue, updatedRightEdgeState, updatedRightEdgePreviousTimestamp) = process(
|
||||
state: self.rightEdgeState,
|
||||
velocity: velocity.x,
|
||||
delta: delta.x,
|
||||
value: rightPoint,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapEdgeLocations.right,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousRightEdgeSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.right, snapped)
|
||||
}
|
||||
}
|
||||
)
|
||||
self.rightEdgeState = updatedRightEdgeState
|
||||
self.previousRightEdgeSnapTimestamp = updatedRightEdgePreviousTimestamp
|
||||
|
||||
updatedPosition.x = updatedXRightEdgeValue - size.width / 2.0
|
||||
}
|
||||
} else {
|
||||
updatedPosition.x = updatedXValue
|
||||
}
|
||||
|
||||
if updatedYState == nil {
|
||||
let (updatedYTopEdgeValue, updatedTopEdgeState, updatedTopEdgePreviousTimestamp) = process(
|
||||
state: self.topEdgeState,
|
||||
velocity: velocity.y,
|
||||
delta: delta.y,
|
||||
value: topPoint,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapEdgeLocations.top,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousTopEdgeSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.top, snapped)
|
||||
}
|
||||
)
|
||||
self.topEdgeState = updatedTopEdgeState
|
||||
self.previousTopEdgeSnapTimestamp = updatedTopEdgePreviousTimestamp
|
||||
|
||||
if updatedTopEdgeState != nil {
|
||||
updatedPosition.y = updatedYTopEdgeValue + size.height / 2.0
|
||||
|
||||
self.bottomEdgeState = nil
|
||||
self.previousBottomEdgeSnapTimestamp = nil
|
||||
} else {
|
||||
let (updatedYBottomEdgeValue, updatedBottomEdgeState, updatedBottomEdgePreviousTimestamp) = process(
|
||||
state: self.bottomEdgeState,
|
||||
velocity: velocity.y,
|
||||
delta: delta.y,
|
||||
value: bottomPoint,
|
||||
snapVelocity: snapVelocity,
|
||||
snapToValue: snapEdgeLocations.bottom,
|
||||
snapDelta: snapDelta,
|
||||
snapSkipTranslation: snapSkipTranslation,
|
||||
previousSnapTimestamp: self.previousBottomEdgeSnapTimestamp,
|
||||
onSnapUpdated: { [weak self] snapped in
|
||||
self?.onSnapUpdated(.bottom, snapped)
|
||||
}
|
||||
)
|
||||
self.bottomEdgeState = updatedBottomEdgeState
|
||||
self.previousBottomEdgeSnapTimestamp = updatedBottomEdgePreviousTimestamp
|
||||
|
||||
updatedPosition.y = updatedYBottomEdgeValue - size.height / 2.0
|
||||
}
|
||||
} else {
|
||||
updatedPosition.y = updatedYValue
|
||||
}
|
||||
} else {
|
||||
self.yState = nil
|
||||
self.onSnapYUpdated(false)
|
||||
updatedPosition.x = updatedXValue
|
||||
updatedPosition.y = updatedYValue
|
||||
}
|
||||
|
||||
return updatedPosition
|
||||
@ -679,7 +850,7 @@ class DrawingEntitySnapTool {
|
||||
updatedRotation = snapRotation
|
||||
} else {
|
||||
self.rotationState = (snapRotation, snapSkipRotation, true)
|
||||
self.onSnapRotationUpdated(nil)
|
||||
self.onSnapUpdated(.rotation(nil), false)
|
||||
}
|
||||
} else {
|
||||
for snapRotation in self.snapRotations {
|
||||
@ -691,7 +862,7 @@ class DrawingEntitySnapTool {
|
||||
self.previousRotationSnapTimestamp = currentTimestamp
|
||||
self.rotationState = (snapRotation, 0.0, false)
|
||||
updatedRotation = snapRotation
|
||||
self.onSnapRotationUpdated(snapRotation)
|
||||
self.onSnapUpdated(.rotation(snapRotation), true)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -699,7 +870,7 @@ class DrawingEntitySnapTool {
|
||||
}
|
||||
} else {
|
||||
self.rotationState = nil
|
||||
self.onSnapRotationUpdated(nil)
|
||||
self.onSnapUpdated(.rotation(nil), false)
|
||||
}
|
||||
|
||||
return updatedRotation
|
||||
|
@ -20,7 +20,7 @@ extension DrawingTextEntity.Alignment {
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
private var textEntity: DrawingTextEntity {
|
||||
return self.entity as! DrawingTextEntity
|
||||
}
|
||||
@ -77,7 +77,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
|
||||
private var isSuspended = false
|
||||
private var _isEditing = false
|
||||
var isEditing: Bool {
|
||||
public var isEditing: Bool {
|
||||
return self._isEditing || self.isSuspended
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else {
|
||||
return
|
||||
}
|
||||
@ -301,7 +301,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
self.textView.selectedRange = NSMakeRange(previousSelectedRange.location + previousSelectedRange.length + text.length, 0)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
self.textView.setNeedsLayersUpdate()
|
||||
var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude))
|
||||
result.width = max(224.0, ceil(result.width) + 20.0)
|
||||
@ -309,7 +309,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
return result;
|
||||
}
|
||||
|
||||
override func sizeToFit() {
|
||||
public override func sizeToFit() {
|
||||
let center = self.center
|
||||
let transform = self.transform
|
||||
self.transform = .identity
|
||||
@ -320,7 +320,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
//entity changed
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.textView.frame = self.bounds
|
||||
@ -506,7 +506,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
override func update(animated: Bool = false) {
|
||||
public override func update(animated: Bool = false) {
|
||||
self.update(animated: animated, afterAppendingEmoji: false)
|
||||
}
|
||||
|
||||
@ -695,21 +695,9 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToAngle(snappedAngle)
|
||||
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
|
||||
if let self, let entityView = self.entityView {
|
||||
entityView.onSnapUpdated(type, snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -753,11 +741,13 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.currentHandle = self.layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
@ -788,7 +778,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
|
||||
}
|
||||
|
||||
entity.scale = updatedScale
|
||||
@ -802,6 +792,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
if self.currentHandle != nil {
|
||||
self.snapTool.rotationReset()
|
||||
}
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -816,11 +807,16 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
if case .began = gestureRecognizer.state {
|
||||
entityView.onInteractionUpdated(true)
|
||||
}
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.scale = max(0.1, entity.scale * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
case .ended, .cancelled:
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -838,6 +834,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
|
||||
entityView.onInteractionUpdated(true)
|
||||
case .changed:
|
||||
rotation = gestureRecognizer.rotation
|
||||
updatedRotation += rotation
|
||||
@ -845,6 +842,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
gestureRecognizer.rotation = 0.0
|
||||
case .ended, .cancelled:
|
||||
self.snapTool.rotationReset()
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -192,6 +192,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
self.currentHandle = layer
|
||||
entityView.onInteractionUpdated(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -245,8 +246,8 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
case .ended, .cancelled:
|
||||
entityView.onInteractionUpdated(false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -621,7 +621,7 @@ private func generateKnobImage() -> UIImage? {
|
||||
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
|
||||
}
|
||||
|
||||
final class TextSizeSliderComponent: Component {
|
||||
public final class TextSizeSliderComponent: Component {
|
||||
let value: CGFloat
|
||||
let tag: AnyObject?
|
||||
let updated: (CGFloat) -> Void
|
||||
@ -646,7 +646,7 @@ final class TextSizeSliderComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
|
||||
public final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
|
||||
private var validSize: CGSize?
|
||||
|
||||
private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
|
||||
@ -739,7 +739,7 @@ final class TextSizeSliderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -787,11 +787,11 @@ final class TextSizeSliderComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
view.updated = self.updated
|
||||
view.released = self.released
|
||||
return view.updateLayout(size: availableSize, component: self, transition: transition)
|
||||
|
@ -259,14 +259,14 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.imageNode.setSignal(imageSignal)
|
||||
|
||||
self.currentDraftState = (draft, index)
|
||||
self.setNeedsLayout()
|
||||
|
||||
if self.typeIconNode.supernode == nil {
|
||||
if self.draftNode.supernode == nil {
|
||||
self.draftNode.attributedText = NSAttributedString(string: "Draft", font: Font.semibold(12.0), textColor: .white)
|
||||
|
||||
self.addSubnode(self.draftNode)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
self.updateSelectionState()
|
||||
@ -290,6 +290,11 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
|
||||
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index {
|
||||
self.currentMediaState = (media.asset, index)
|
||||
|
||||
if self.draftNode.supernode != nil {
|
||||
self.draftNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
@ -313,6 +318,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index {
|
||||
self.backgroundNode.image = nil
|
||||
let editingContext = interaction.editingState
|
||||
let asset = fetchResult.object(at: index)
|
||||
|
||||
|
@ -487,7 +487,12 @@ public final class EngineStorySubscriptions: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
public enum StoryUploadResult {
|
||||
case progress(Float)
|
||||
case completed
|
||||
}
|
||||
|
||||
func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy) -> Signal<StoryUploadResult, NoError> {
|
||||
let originalMedia: Media
|
||||
let contentToUpload: MessageContentToUpload
|
||||
|
||||
@ -569,47 +574,53 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<Never, NoError> in
|
||||
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||
var privacyRules: [Api.InputPrivacyRule]
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyRules = [.inputPrivacyValueAllowAll]
|
||||
case .contacts:
|
||||
privacyRules = [.inputPrivacyValueAllowContacts]
|
||||
case .closeFriends:
|
||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||
case .nobody:
|
||||
privacyRules = [.inputPrivacyValueDisallowAll]
|
||||
}
|
||||
var privacyUsers: [Api.InputUser] = []
|
||||
var privacyChats: [Int64] = []
|
||||
for peerId in privacy.additionallyIncludePeers {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
if let _ = peer as? TelegramUser {
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
privacyUsers.append(inputUser)
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
return account.postbox.transaction { transaction -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
return .single(.progress(progress))
|
||||
case let .content(content):
|
||||
var privacyRules: [Api.InputPrivacyRule]
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyRules = [.inputPrivacyValueAllowAll]
|
||||
case .contacts:
|
||||
privacyRules = [.inputPrivacyValueAllowContacts]
|
||||
case .closeFriends:
|
||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||
case .nobody:
|
||||
privacyRules = [.inputPrivacyValueDisallowAll]
|
||||
}
|
||||
var privacyUsers: [Api.InputUser] = []
|
||||
var privacyChats: [Int64] = []
|
||||
for peerId in privacy.additionallyIncludePeers {
|
||||
if let peer = transaction.getPeer(peerId) {
|
||||
if let _ = peer as? TelegramUser {
|
||||
if let inputUser = apiInputUser(peer) {
|
||||
privacyUsers.append(inputUser)
|
||||
}
|
||||
} else if peer is TelegramGroup || peer is TelegramChannel {
|
||||
privacyChats.append(peer.id.id._internalGetInt64Value())
|
||||
}
|
||||
} else if peer is TelegramGroup || peer is TelegramChannel {
|
||||
privacyChats.append(peer.id.id._internalGetInt64Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !privacyUsers.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
|
||||
}
|
||||
if !privacyChats.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .content(content):
|
||||
if !privacyUsers.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers))
|
||||
}
|
||||
if !privacyChats.isEmpty {
|
||||
privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats))
|
||||
}
|
||||
|
||||
switch content.content {
|
||||
case let .media(inputMedia, _):
|
||||
var flags: Int32 = 0
|
||||
var apiCaption: String?
|
||||
var apiEntities: [Api.MessageEntity]?
|
||||
|
||||
if pin {
|
||||
flags |= 1 << 2
|
||||
}
|
||||
|
||||
if !text.isEmpty {
|
||||
flags |= 1 << 0
|
||||
apiCaption = text
|
||||
@ -641,7 +652,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { updates -> Signal<Never, NoError> in
|
||||
|> mapToSignal { updates -> Signal<StoryUploadResult, NoError> in
|
||||
if let updates = updates {
|
||||
for update in updates.allUpdates {
|
||||
if case let .updateStory(_, story) = update {
|
||||
@ -660,7 +671,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text:
|
||||
account.stateManager.addUpdates(updates)
|
||||
}
|
||||
|
||||
return .complete()
|
||||
return .single(.completed)
|
||||
}
|
||||
default:
|
||||
return .complete()
|
||||
|
@ -859,8 +859,8 @@ public extension TelegramEngine {
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal<Never, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, privacy: privacy)
|
||||
public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy) -> Signal<StoryUploadResult, NoError> {
|
||||
return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy)
|
||||
}
|
||||
|
||||
public func deleteStory(id: Int32) -> Signal<Never, NoError> {
|
||||
|
@ -203,12 +203,12 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
} else {
|
||||
self.camera.setFlashMode(.off)
|
||||
}
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
func togglePosition() {
|
||||
self.camera.togglePosition()
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
||||
@ -787,6 +787,10 @@ public class CameraScreen: ViewController {
|
||||
).start(next: { [weak self] changingPosition, forceBlur in
|
||||
if let self {
|
||||
if changingPosition {
|
||||
if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) {
|
||||
self.simplePreviewView?.addSubview(snapshot)
|
||||
self.previewSnapshotView = snapshot
|
||||
}
|
||||
UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
})
|
||||
@ -879,6 +883,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var isDismissing = false
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
@ -891,7 +896,8 @@ public class CameraScreen: ViewController {
|
||||
if !"".isEmpty {
|
||||
|
||||
} else {
|
||||
if translation.x < -10.0 {
|
||||
if translation.x < -10.0 || self.isDismissing {
|
||||
self.isDismissing = true
|
||||
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
|
||||
controller.updateTransitionProgress(transitionFraction, transition: .immediate)
|
||||
} else if translation.y < -10.0 {
|
||||
@ -904,6 +910,8 @@ public class CameraScreen: ViewController {
|
||||
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
|
||||
controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true)
|
||||
|
||||
self.isDismissing = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -1306,7 +1314,7 @@ public class CameraScreen: ViewController {
|
||||
|
||||
func presentGallery(fromGesture: Bool = false) {
|
||||
if !fromGesture {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
var didStopCameraCapture = false
|
||||
@ -1364,8 +1372,10 @@ public class CameraScreen: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
if !interactive {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
self.hapticFeedback.impact(.light)
|
||||
}
|
||||
|
||||
self.node.camera.stopCapture(invalidate: true)
|
||||
@ -1385,6 +1395,20 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissAllTooltips() {
|
||||
self.window?.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
})
|
||||
self.forEachController({ controller in
|
||||
if let controller = controller as? TooltipScreen {
|
||||
controller.dismiss()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
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))
|
||||
|
@ -214,7 +214,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
self.colorPixelFormat = .bgra8Unorm
|
||||
self.framebufferOnly = true
|
||||
//self.presentsWithTransaction = true
|
||||
|
||||
self.isPaused = true
|
||||
self.delegate = self
|
||||
|
||||
|
@ -134,6 +134,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
public let networkStatus: HeaderNetworkStatusComponent.Content?
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let storiesFraction: CGFloat
|
||||
public let uploadProgress: Float?
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
@ -149,6 +150,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
networkStatus: HeaderNetworkStatusComponent.Content?,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
storiesFraction: CGFloat,
|
||||
uploadProgress: Float?,
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
@ -163,6 +165,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.networkStatus = networkStatus
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.storiesFraction = storiesFraction
|
||||
self.uploadProgress = uploadProgress
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.openStatusSetup = openStatusSetup
|
||||
@ -191,6 +194,9 @@ public final class ChatListHeaderComponent: Component {
|
||||
if lhs.storiesFraction != rhs.storiesFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.uploadProgress != rhs.uploadProgress {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@ -789,6 +795,9 @@ public final class ChatListHeaderComponent: Component {
|
||||
self.storyPeerList = storyPeerList
|
||||
}
|
||||
|
||||
if let uploadProgress = component.uploadProgress {
|
||||
print("out \(uploadProgress)")
|
||||
}
|
||||
let _ = storyPeerList.update(
|
||||
transition: storyListTransition,
|
||||
component: AnyComponent(StoryPeerListComponent(
|
||||
@ -798,6 +807,7 @@ public final class ChatListHeaderComponent: Component {
|
||||
strings: component.strings,
|
||||
storySubscriptions: storySubscriptions,
|
||||
collapseFraction: 1.0 - component.storiesFraction,
|
||||
uploadProgress: component.uploadProgress,
|
||||
peerAction: { [weak self] peer in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -21,6 +21,7 @@ public final class ChatListNavigationBar: Component {
|
||||
public let secondaryContent: ChatListHeaderComponent.Content?
|
||||
public let secondaryTransition: CGFloat
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let uploadProgress: Float?
|
||||
public let tabsNode: ASDisplayNode?
|
||||
public let activateSearch: (NavigationBarSearchContentNode) -> Void
|
||||
public let openStatusSetup: (UIView) -> Void
|
||||
@ -37,6 +38,7 @@ public final class ChatListNavigationBar: Component {
|
||||
secondaryContent: ChatListHeaderComponent.Content?,
|
||||
secondaryTransition: CGFloat,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
uploadProgress: Float?,
|
||||
tabsNode: ASDisplayNode?,
|
||||
activateSearch: @escaping (NavigationBarSearchContentNode) -> Void,
|
||||
openStatusSetup: @escaping (UIView) -> Void
|
||||
@ -52,6 +54,7 @@ public final class ChatListNavigationBar: Component {
|
||||
self.secondaryContent = secondaryContent
|
||||
self.secondaryTransition = secondaryTransition
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.uploadProgress = uploadProgress
|
||||
self.tabsNode = tabsNode
|
||||
self.activateSearch = activateSearch
|
||||
self.openStatusSetup = openStatusSetup
|
||||
@ -91,6 +94,9 @@ public final class ChatListNavigationBar: Component {
|
||||
if lhs.storySubscriptions != rhs.storySubscriptions {
|
||||
return false
|
||||
}
|
||||
if lhs.uploadProgress != rhs.uploadProgress {
|
||||
return false
|
||||
}
|
||||
if lhs.tabsNode != rhs.tabsNode {
|
||||
return false
|
||||
}
|
||||
@ -166,7 +172,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func applyScroll(offset: CGFloat, transition: Transition) {
|
||||
public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) {
|
||||
var transition = transition
|
||||
if self.applyScrollFractionAnimator != nil {
|
||||
transition = .immediate
|
||||
@ -174,7 +180,7 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
self.rawScrollOffset = offset
|
||||
|
||||
if self.deferScrollApplication {
|
||||
if self.deferScrollApplication && !forceUpdate {
|
||||
self.hasDeferredScrollOffset = true
|
||||
return
|
||||
}
|
||||
@ -201,7 +207,7 @@ public final class ChatListNavigationBar: Component {
|
||||
}
|
||||
|
||||
let clippedScrollOffset = min(minContentOffset, offset)
|
||||
if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset {
|
||||
if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate {
|
||||
return
|
||||
}
|
||||
self.hasDeferredScrollOffset = false
|
||||
@ -286,6 +292,7 @@ public final class ChatListNavigationBar: Component {
|
||||
networkStatus: nil,
|
||||
storySubscriptions: component.storySubscriptions,
|
||||
storiesFraction: 1.0 - storiesOffsetFraction,
|
||||
uploadProgress: component.uploadProgress,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
@ -367,8 +374,14 @@ public final class ChatListNavigationBar: Component {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var storiesUnlockedUpdated = false
|
||||
if let previousComponent = self.component, previousComponent.storiesUnlocked != component.storiesUnlocked {
|
||||
storiesUnlockedUpdated = true
|
||||
var uploadProgressUpdated = false
|
||||
if let previousComponent = self.component {
|
||||
if previousComponent.storiesUnlocked != component.storiesUnlocked {
|
||||
storiesUnlockedUpdated = true
|
||||
}
|
||||
if previousComponent.uploadProgress != component.uploadProgress {
|
||||
uploadProgressUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
@ -412,6 +425,12 @@ public final class ChatListNavigationBar: Component {
|
||||
|
||||
self.hasDeferredScrollOffset = true
|
||||
|
||||
if uploadProgressUpdated {
|
||||
if let rawScrollOffset = self.rawScrollOffset {
|
||||
self.applyScroll(offset: rawScrollOffset, forceUpdate: true, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
if storiesUnlockedUpdated {
|
||||
self.applyScrollFraction = 0.0
|
||||
self.applyScrollStartFraction = 0.0
|
||||
|
@ -1,6 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
public enum CodableDrawingEntity {
|
||||
public enum CodableDrawingEntity: Equatable {
|
||||
public static func == (lhs: CodableDrawingEntity, rhs: CodableDrawingEntity) -> Bool {
|
||||
return lhs.entity.isEqual(to: rhs.entity)
|
||||
}
|
||||
|
||||
case sticker(DrawingStickerEntity)
|
||||
case text(DrawingTextEntity)
|
||||
case simpleShape(DrawingSimpleShapeEntity)
|
||||
|
@ -104,4 +104,38 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable {
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingBubbleEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.drawType != other.drawType {
|
||||
return false
|
||||
}
|
||||
if self.color != other.color {
|
||||
return false
|
||||
}
|
||||
if self.lineWidth != other.lineWidth {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.position != other.position {
|
||||
return false
|
||||
}
|
||||
if self.size != other.size {
|
||||
return false
|
||||
}
|
||||
if self.rotation != other.rotation {
|
||||
return false
|
||||
}
|
||||
if self.tailPosition != other.tailPosition {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -17,4 +17,6 @@ public protocol DrawingEntity: AnyObject {
|
||||
|
||||
var renderImage: UIImage? { get set }
|
||||
var renderSubEntities: [DrawingEntity]? { get set }
|
||||
|
||||
func isEqual(to other: DrawingEntity) -> Bool
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import AccountContext
|
||||
import Photos
|
||||
|
||||
public final class DrawingMediaEntity: DrawingEntity, Codable {
|
||||
public enum Content {
|
||||
public enum Content: Equatable {
|
||||
case image(UIImage, PixelDimensions)
|
||||
case video(String, PixelDimensions)
|
||||
case asset(PHAsset)
|
||||
@ -20,6 +20,29 @@ public final class DrawingMediaEntity: DrawingEntity, Codable {
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
case let .image(lhsImage, lhsDimensions):
|
||||
if case let .image(rhsImage, rhsDimensions) = rhs {
|
||||
return lhsImage === rhsImage && lhsDimensions == rhsDimensions
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .video(lhsPath, lhsDimensions):
|
||||
if case let .video(rhsPath, rhsDimensions) = rhs {
|
||||
return lhsPath == rhsPath && lhsDimensions == rhsDimensions
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .asset(lhsAsset):
|
||||
if case let .asset(rhsAsset) = rhs {
|
||||
return lhsAsset.localIdentifier == rhsAsset.localIdentifier
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@ -143,4 +166,35 @@ public final class DrawingMediaEntity: DrawingEntity, Codable {
|
||||
newEntity.mirrored = self.mirrored
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingMediaEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.content != other.content {
|
||||
return false
|
||||
}
|
||||
if self.size != other.size {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.position != other.position {
|
||||
return false
|
||||
}
|
||||
if self.scale != other.scale {
|
||||
return false
|
||||
}
|
||||
if self.rotation != other.rotation {
|
||||
return false
|
||||
}
|
||||
if self.mirrored != other.mirrored {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -110,4 +110,38 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingSimpleShapeEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.shapeType != other.shapeType {
|
||||
return false
|
||||
}
|
||||
if self.drawType != other.drawType {
|
||||
return false
|
||||
}
|
||||
if self.color != other.color {
|
||||
return false
|
||||
}
|
||||
if self.lineWidth != other.lineWidth {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.position != other.position {
|
||||
return false
|
||||
}
|
||||
if self.size != other.size {
|
||||
return false
|
||||
}
|
||||
if self.rotation != other.rotation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,26 @@ import AccountContext
|
||||
import TelegramCore
|
||||
|
||||
public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
public enum Content {
|
||||
public enum Content: Equatable {
|
||||
case file(TelegramMediaFile)
|
||||
case image(UIImage)
|
||||
|
||||
public static func == (lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
case let .file(lhsFile):
|
||||
if case let .file(rhsFile) = rhs {
|
||||
return lhsFile.fileId == rhsFile.fileId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .image(lhsImage):
|
||||
if case let .image(rhsImage) = rhs {
|
||||
return lhsImage === rhsImage
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
@ -110,4 +127,32 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
newEntity.mirrored = self.mirrored
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingStickerEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.content != other.content {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.position != other.position {
|
||||
return false
|
||||
}
|
||||
if self.scale != other.scale {
|
||||
return false
|
||||
}
|
||||
if self.rotation != other.rotation {
|
||||
return false
|
||||
}
|
||||
if self.mirrored != other.mirrored {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -56,26 +56,26 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
case renderAnimationFrames
|
||||
}
|
||||
|
||||
public enum Style: Codable {
|
||||
public enum Style: Codable, Equatable {
|
||||
case regular
|
||||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
}
|
||||
|
||||
public enum Animation: Codable {
|
||||
public enum Animation: Codable, Equatable {
|
||||
case none
|
||||
case typing
|
||||
case wiggle
|
||||
case zoomIn
|
||||
}
|
||||
|
||||
public enum Font: Codable {
|
||||
public enum Font: Codable, Equatable {
|
||||
case sanFrancisco
|
||||
case other(String, String)
|
||||
}
|
||||
|
||||
public enum Alignment: Codable {
|
||||
public enum Alignment: Codable, Equatable {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
@ -257,6 +257,52 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingTextEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.text != other.text {
|
||||
return false
|
||||
}
|
||||
if self.style != other.style {
|
||||
return false
|
||||
}
|
||||
if self.animation != other.animation {
|
||||
return false
|
||||
}
|
||||
if self.font != other.font {
|
||||
return false
|
||||
}
|
||||
if self.alignment != other.alignment {
|
||||
return false
|
||||
}
|
||||
if self.fontSize != other.fontSize {
|
||||
return false
|
||||
}
|
||||
if self.color != other.color {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.position != other.position {
|
||||
return false
|
||||
}
|
||||
if self.width != other.width {
|
||||
return false
|
||||
}
|
||||
if self.scale != other.scale {
|
||||
return false
|
||||
}
|
||||
if self.rotation != other.rotation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// public weak var currentEntityView: DrawingEntityView?
|
||||
// public func makeView(context: AccountContext) -> DrawingEntityView {
|
||||
// let entityView = DrawingTextEntityView(context: context, entity: self)
|
||||
|
@ -107,4 +107,38 @@ public final class DrawingVectorEntity: DrawingEntity, Codable {
|
||||
newEntity.end = self.end
|
||||
return newEntity
|
||||
}
|
||||
|
||||
public func isEqual(to other: DrawingEntity) -> Bool {
|
||||
guard let other = other as? DrawingVectorEntity else {
|
||||
return false
|
||||
}
|
||||
if self.uuid != other.uuid {
|
||||
return false
|
||||
}
|
||||
if self.type != other.type {
|
||||
return false
|
||||
}
|
||||
if self.color != other.color {
|
||||
return false
|
||||
}
|
||||
if self.lineWidth != other.lineWidth {
|
||||
return false
|
||||
}
|
||||
if self.drawingSize != other.drawingSize {
|
||||
return false
|
||||
}
|
||||
if self.referenceDrawingSize != other.referenceDrawingSize {
|
||||
return false
|
||||
}
|
||||
if self.start != other.start {
|
||||
return false
|
||||
}
|
||||
if self.mid.0 != other.mid.0 || self.mid.1 != other.mid.1 {
|
||||
return false
|
||||
}
|
||||
if self.end != other.end {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ final class ImageTextureSource: TextureSource {
|
||||
func connect(to consumer: TextureConsumer) {
|
||||
self.output = consumer
|
||||
if let texture = self.texture {
|
||||
self.output?.consumeTexture(texture)
|
||||
self.output?.consumeTexture(texture, render: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ public final class MediaEditor {
|
||||
let duration = asset.duration.seconds
|
||||
let interval = duration / Double(count)
|
||||
for i in 0 ..< count {
|
||||
timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(60.0))))
|
||||
timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(NSEC_PER_SEC))))
|
||||
}
|
||||
|
||||
var updatedFrames: [UIImage] = []
|
||||
@ -396,7 +396,7 @@ public final class MediaEditor {
|
||||
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
|
||||
if let self {
|
||||
let start = self.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: 60))
|
||||
self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
|
||||
self.player?.play()
|
||||
}
|
||||
})
|
||||
@ -449,7 +449,7 @@ public final class MediaEditor {
|
||||
if !play {
|
||||
self.player?.pause()
|
||||
}
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
if self.targetTimePosition?.0 != targetPosition {
|
||||
self.targetTimePosition = (targetPosition, play)
|
||||
if !self.updatingTimePosition {
|
||||
@ -487,17 +487,21 @@ public final class MediaEditor {
|
||||
}
|
||||
|
||||
public func setVideoTrimStart(_ trimStart: Double) {
|
||||
self.skipRendering = true
|
||||
let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
self.values = self.values.withUpdatedVideoTrimRange(trimRange)
|
||||
self.skipRendering = false
|
||||
}
|
||||
|
||||
public func setVideoTrimEnd(_ trimEnd: Double) {
|
||||
self.skipRendering = true
|
||||
let trimStart = self.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
self.values = self.values.withUpdatedVideoTrimRange(trimRange)
|
||||
|
||||
self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimEnd, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
||||
self.skipRendering = false
|
||||
}
|
||||
|
||||
public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) {
|
||||
@ -508,11 +512,30 @@ public final class MediaEditor {
|
||||
self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors)
|
||||
}
|
||||
|
||||
private var previousUpdateTime = CACurrentMediaTime()
|
||||
private var scheduledUpdate = false
|
||||
private func updateRenderChain() {
|
||||
self.renderChain.update(values: self.values)
|
||||
if let player = self.player, player.rate > 0.0 {
|
||||
} else {
|
||||
self.previewView?.scheduleFrame()
|
||||
let currentTime = CACurrentMediaTime()
|
||||
if !self.scheduledUpdate {
|
||||
let delay = 0.03333
|
||||
let delta = currentTime - self.previousUpdateTime
|
||||
if delta < delay {
|
||||
self.scheduledUpdate = true
|
||||
Queue.mainQueue().after(delay - delta) {
|
||||
self.scheduledUpdate = false
|
||||
self.previousUpdateTime = CACurrentMediaTime()
|
||||
self.renderer.willRenderFrame()
|
||||
self.renderer.renderFrame()
|
||||
}
|
||||
} else {
|
||||
self.previousUpdateTime = currentTime
|
||||
self.renderer.willRenderFrame()
|
||||
self.renderer.renderFrame()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -564,7 +587,8 @@ final class MediaEditorRenderChain {
|
||||
}
|
||||
|
||||
func update(values: MediaEditorValues) {
|
||||
for (key, value) in values.toolValues {
|
||||
for key in EditorToolKey.allCases {
|
||||
let value = values.toolValues[key]
|
||||
switch key {
|
||||
case .enhance:
|
||||
if let value = value as? Float {
|
||||
|
@ -101,8 +101,7 @@ final class MediaEditorComposer {
|
||||
}
|
||||
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
|
||||
|
||||
self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation)
|
||||
self.renderer.renderFrame()
|
||||
self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation, render: true)
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
@ -136,8 +135,7 @@ final class MediaEditorComposer {
|
||||
}
|
||||
if self.filteredImage == nil, let device = self.device {
|
||||
if let texture = loadTexture(image: inputImage, device: device) {
|
||||
self.renderer.consumeTexture(texture)
|
||||
self.renderer.renderFrame()
|
||||
self.renderer.consumeTexture(texture, render: true)
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
@ -14,7 +14,19 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
|
||||
var drawable: MTLDrawable? {
|
||||
return self.currentDrawable
|
||||
return self.nextDrawable
|
||||
}
|
||||
|
||||
var nextDrawable: MTLDrawable? {
|
||||
if #available(iOS 13.0, *) {
|
||||
if let layer = self.layer as? CAMetalLayer {
|
||||
return layer.nextDrawable()
|
||||
} else {
|
||||
return self.currentDrawable
|
||||
}
|
||||
} else {
|
||||
return self.currentDrawable
|
||||
}
|
||||
}
|
||||
|
||||
var renderPassDescriptor: MTLRenderPassDescriptor? {
|
||||
@ -46,13 +58,8 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
self.colorPixelFormat = .bgra8Unorm
|
||||
|
||||
self.isPaused = true
|
||||
self.enableSetNeedsDisplay = false
|
||||
}
|
||||
|
||||
func scheduleFrame() {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.draw()
|
||||
}
|
||||
self.enableSetNeedsDisplay = true
|
||||
self.framebufferOnly = true
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
@ -61,10 +68,14 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
}
|
||||
|
||||
public func redraw() {
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func draw(in view: MTKView) {
|
||||
guard self.frame.width > 0.0 else {
|
||||
return
|
||||
}
|
||||
self.renderer?.renderFrame()
|
||||
self.renderer?.displayFrame()
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import Photos
|
||||
import SwiftSignalKit
|
||||
|
||||
protocol TextureConsumer: AnyObject {
|
||||
func consumeTexture(_ texture: MTLTexture)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation)
|
||||
func consumeTexture(_ texture: MTLTexture, render: Bool)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, render: Bool)
|
||||
}
|
||||
|
||||
final class RenderingContext {
|
||||
@ -40,7 +40,7 @@ protocol RenderTarget: AnyObject {
|
||||
var drawable: MTLDrawable? { get }
|
||||
var renderPassDescriptor: MTLRenderPassDescriptor? { get }
|
||||
|
||||
func scheduleFrame()
|
||||
func redraw()
|
||||
}
|
||||
|
||||
final class MediaEditorRenderer: TextureConsumer {
|
||||
@ -161,10 +161,12 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
guard let device = device,
|
||||
let commandQueue = self.commandQueue,
|
||||
let textureCache = self.textureCache else {
|
||||
self.semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
guard let commandBuffer = commandQueue.makeCommandBuffer() else {
|
||||
self.semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
@ -174,6 +176,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
} else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) {
|
||||
texture = videoTexture
|
||||
} else {
|
||||
self.semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
@ -182,61 +185,75 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
texture = nextTexture
|
||||
}
|
||||
}
|
||||
if self.renderTarget != nil {
|
||||
self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
self.finalTexture = texture
|
||||
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
if let self {
|
||||
if self.renderTarget == nil {
|
||||
self.semaphore.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
commandBuffer.commit()
|
||||
|
||||
if let renderTarget = self.renderTarget {
|
||||
renderTarget.redraw()
|
||||
} else {
|
||||
commandBuffer.waitUntilCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
func displayFrame() {
|
||||
guard let renderTarget = self.renderTarget,
|
||||
let device = renderTarget.mtlDevice,
|
||||
let commandQueue = self.commandQueue,
|
||||
let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||
let texture = self.finalTexture
|
||||
else {
|
||||
self.semaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
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
|
||||
self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
if let _ = self.renderTarget {
|
||||
commandBuffer.commit()
|
||||
commandBuffer.waitUntilScheduled()
|
||||
} else {
|
||||
commandBuffer.commit()
|
||||
commandBuffer.waitUntilCompleted()
|
||||
}
|
||||
commandBuffer.commit()
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture) {
|
||||
self.semaphore.wait()
|
||||
func willRenderFrame() {
|
||||
let _ = self.semaphore.wait(timeout: .distantFuture)
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture, render: Bool) {
|
||||
if render {
|
||||
let _ = self.semaphore.wait(timeout: .distantFuture)
|
||||
}
|
||||
|
||||
self.currentTexture = texture
|
||||
self.renderTarget?.scheduleFrame()
|
||||
if render {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) {
|
||||
self.semaphore.wait()
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, render: Bool) {
|
||||
let _ = self.semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
self.currentPixelBuffer = (pixelBuffer, rotation)
|
||||
self.renderTarget?.scheduleFrame()
|
||||
if render {
|
||||
self.renderFrame()
|
||||
}
|
||||
}
|
||||
|
||||
func renderTargetDidChange(_ target: RenderTarget?) {
|
||||
@ -245,7 +262,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
|
||||
func renderTargetDrawableSizeDidChange(_ size: CGSize) {
|
||||
self.renderTarget?.scheduleFrame()
|
||||
self.renderTarget?.redraw()
|
||||
}
|
||||
|
||||
func finalRenderedImage() -> UIImage? {
|
||||
|
@ -5,7 +5,7 @@ import TelegramCore
|
||||
import AVFoundation
|
||||
import VideoToolbox
|
||||
|
||||
public enum EditorToolKey: Int32 {
|
||||
public enum EditorToolKey: Int32, CaseIterable {
|
||||
case enhance
|
||||
case brightness
|
||||
case contrast
|
||||
@ -37,7 +37,50 @@ private let adjustmentToolsKeys: [EditorToolKey] = [
|
||||
.sharpen
|
||||
]
|
||||
|
||||
public final class MediaEditorValues: Codable {
|
||||
public final class MediaEditorValues: Codable, Equatable {
|
||||
public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool {
|
||||
if lhs.originalDimensions != rhs.originalDimensions {
|
||||
return false
|
||||
}
|
||||
if lhs.cropOffset != rhs.cropOffset {
|
||||
return false
|
||||
}
|
||||
if lhs.cropSize != rhs.cropSize {
|
||||
return false
|
||||
}
|
||||
if lhs.cropScale != rhs.cropScale {
|
||||
return false
|
||||
}
|
||||
if lhs.cropRotation != rhs.cropRotation {
|
||||
return false
|
||||
}
|
||||
if lhs.cropMirroring != rhs.cropMirroring {
|
||||
return false
|
||||
}
|
||||
if lhs.gradientColors != rhs.gradientColors {
|
||||
return false
|
||||
}
|
||||
if lhs.videoTrimRange != rhs.videoTrimRange {
|
||||
return false
|
||||
}
|
||||
if lhs.videoIsMuted != rhs.videoIsMuted {
|
||||
return false
|
||||
}
|
||||
if lhs.videoIsFullHd != rhs.videoIsFullHd {
|
||||
return false
|
||||
}
|
||||
if lhs.drawing !== rhs.drawing {
|
||||
return false
|
||||
}
|
||||
if lhs.entities != rhs.entities {
|
||||
return false
|
||||
}
|
||||
// if lhs.toolValues != rhs.toolValues {
|
||||
// return false
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case originalWidth
|
||||
case originalHeight
|
||||
@ -211,6 +254,39 @@ public final class MediaEditorValues: Codable {
|
||||
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, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: toolValues)
|
||||
}
|
||||
|
||||
public var resultDimensions: PixelDimensions {
|
||||
if self.videoIsFullHd {
|
||||
return PixelDimensions(width: 1080, height: 1920)
|
||||
} else {
|
||||
return PixelDimensions(width: 720, height: 1280)
|
||||
}
|
||||
}
|
||||
|
||||
public var hasChanges: Bool {
|
||||
if self.cropOffset != .zero {
|
||||
return true
|
||||
}
|
||||
if self.cropScale != 1.0 {
|
||||
return true
|
||||
}
|
||||
if self.cropRotation != 0.0 {
|
||||
return true
|
||||
}
|
||||
if self.cropMirroring {
|
||||
return true
|
||||
}
|
||||
if self.videoTrimRange != nil {
|
||||
return true
|
||||
}
|
||||
if !self.entities.isEmpty {
|
||||
return true
|
||||
}
|
||||
if !self.toolValues.isEmpty {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public struct TintValue: Equatable, Codable {
|
||||
@ -621,7 +697,7 @@ public extension MediaEditorValues {
|
||||
}
|
||||
|
||||
var requiresComposing: Bool {
|
||||
if self.originalDimensions.width > self.originalDimensions.height {
|
||||
if self.originalDimensions.width > 0 && abs((Double(self.originalDimensions.height) / Double(self.originalDimensions.width)) - 1.7777778) > 0.001 {
|
||||
return true
|
||||
}
|
||||
if abs(1.0 - self.cropScale) > 0.0 {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
import Metal
|
||||
import simd
|
||||
|
||||
@ -133,31 +134,41 @@ final class OutputRenderPass: DefaultRenderPass {
|
||||
|
||||
@discardableResult
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else {
|
||||
guard let renderTarget = self.renderTarget else {
|
||||
return nil
|
||||
}
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let drawableSize = renderTarget.drawableSize
|
||||
|
||||
let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(
|
||||
descriptor: renderPassDescriptor)!
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0.0, originY: 0.0,
|
||||
width: Double(drawableSize.width), height: Double(drawableSize.height),
|
||||
znear: -1.0, zfar: 1.0))
|
||||
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let drawable = renderTarget.drawable {
|
||||
autoreleasepool {
|
||||
guard let drawable = renderTarget.drawable else {
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
|
||||
let drawableSize = renderTarget.drawableSize
|
||||
|
||||
let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(
|
||||
descriptor: renderPassDescriptor)!
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0.0, originY: 0.0,
|
||||
width: Double(drawableSize.width), height: Double(drawableSize.height),
|
||||
znear: -1.0, zfar: 1.0))
|
||||
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -98,16 +98,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
return
|
||||
}
|
||||
|
||||
var frameRate: Int = 30
|
||||
var hasVideoTrack: Bool = false
|
||||
for track in playerItem.asset.tracks {
|
||||
if track.mediaType == .video {
|
||||
if track.nominalFrameRate > 0.0 {
|
||||
frameRate = Int(ceil(track.nominalFrameRate))
|
||||
}
|
||||
hasVideoTrack = true
|
||||
break
|
||||
}
|
||||
}
|
||||
self.textureRotation = textureRotatonForAVAsset(playerItem.asset)
|
||||
if !hasVideoTrack {
|
||||
assertionFailure("No video track found.")
|
||||
return
|
||||
}
|
||||
|
||||
@ -129,7 +132,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
playerItem.add(output)
|
||||
self.playerItemOutput = output
|
||||
|
||||
self.setupDisplayLink()
|
||||
self.setupDisplayLink(frameRate: min(60, frameRate))
|
||||
}
|
||||
|
||||
private class DisplayLinkTarget {
|
||||
@ -142,7 +145,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
}
|
||||
}
|
||||
|
||||
private func setupDisplayLink() {
|
||||
private func setupDisplayLink(frameRate: Int) {
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
|
||||
@ -150,7 +153,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in
|
||||
self?.handleUpdate()
|
||||
}), selector: #selector(DisplayLinkTarget.handleDisplayLinkUpdate(sender:)))
|
||||
displayLink.preferredFramesPerSecond = 60
|
||||
displayLink.preferredFramesPerSecond = frameRate
|
||||
displayLink.add(to: .main, forMode: .common)
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
@ -183,7 +186,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
var presentationTime: CMTime = .zero
|
||||
if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) {
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation)
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation, render: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,297 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AvatarNode
|
||||
import AccountContext
|
||||
import MessageInputPanelComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private final class AvatarComponent: Component {
|
||||
let context: AccountContext
|
||||
let peer: EnginePeer
|
||||
|
||||
init(context: AccountContext, peer: EnginePeer) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var component: AvatarComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 36.0, height: 36.0)
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.avatarNode.setPeer(
|
||||
context: component.context,
|
||||
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
|
||||
peer: component.peer,
|
||||
synchronousLoad: true
|
||||
)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class StoryPreviewComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let context: AccountContext
|
||||
let caption: String
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
caption: String
|
||||
) {
|
||||
self.context = context
|
||||
self.caption = caption
|
||||
}
|
||||
|
||||
static func ==(lhs: StoryPreviewComponent, rhs: StoryPreviewComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.caption != rhs.caption {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
private let context: AccountContext
|
||||
private var peerDisposable: Disposable?
|
||||
fileprivate var accountPeer: EnginePeer?
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.peerDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let self {
|
||||
self.accountPeer = peer
|
||||
self.updated()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peerDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(
|
||||
context: self.context
|
||||
)
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let line = ComponentView<Empty>()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let avatar = ComponentView<Empty>()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
private let inputPanel = ComponentView<Empty>()
|
||||
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
|
||||
|
||||
private let scrubber = ComponentView<Empty>()
|
||||
|
||||
private var component: StoryPreviewComponent?
|
||||
private weak var state: State?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StoryPreviewComponent, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let lineSize = self.line.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Rectangle(color: UIColor(white: 1.0, alpha: 0.5))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 2.0)
|
||||
)
|
||||
let lineFrame = CGRect(
|
||||
origin: CGPoint(x: 8.0, y: 8.0),
|
||||
size: lineSize
|
||||
)
|
||||
if let lineView = self.line.view {
|
||||
if lineView.superview == nil {
|
||||
lineView.layer.cornerRadius = 1.0
|
||||
self.addSubview(lineView)
|
||||
}
|
||||
transition.setPosition(view: lineView, position: lineFrame.center)
|
||||
transition.setBounds(view: lineView, bounds: CGRect(origin: .zero, size: lineFrame.size))
|
||||
}
|
||||
|
||||
let cancelButtonSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Media Gallery/Close",
|
||||
tintColor: UIColor.white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(
|
||||
origin: CGPoint(x: 17.0, y: 24.0),
|
||||
size: cancelButtonSize
|
||||
)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center)
|
||||
transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size))
|
||||
}
|
||||
|
||||
if let accountPeer = state.accountPeer {
|
||||
let avatarSize = self.avatar.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(AvatarComponent(
|
||||
context: component.context,
|
||||
peer: accountPeer
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let avatarFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - avatarSize.width - 6.0, y: 14.0),
|
||||
size: avatarSize
|
||||
)
|
||||
if let avatarView = self.avatar.view {
|
||||
if avatarView.superview == nil {
|
||||
self.addSubview(avatarView)
|
||||
}
|
||||
transition.setPosition(view: avatarView, position: avatarFrame.center)
|
||||
transition.setBounds(view: avatarView, bounds: CGRect(origin: .zero, size: avatarFrame.size))
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Text(
|
||||
text: "My story",
|
||||
font: Font.semibold(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 180.0, height: 44.0)
|
||||
)
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 21.0),
|
||||
size: titleSize
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.center)
|
||||
transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleFrame.size))
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputPanelComponent(
|
||||
externalState: self.inputPanelExternalState,
|
||||
context: component.context,
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
style: .story,
|
||||
placeholder: "Reply Privately...",
|
||||
presentController: { _ in
|
||||
},
|
||||
sendMessageAction: {
|
||||
},
|
||||
setMediaRecordingActive: { _, _, _ in },
|
||||
lockMediaRecording: nil,
|
||||
stopAndPreviewMediaRecording: nil,
|
||||
discardMediaRecordingPreview: nil,
|
||||
attachmentAction: { },
|
||||
reactionAction: { _ in },
|
||||
timeoutAction: nil,
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
recordedAudioPreview: nil,
|
||||
wasRecordingDismissed: false,
|
||||
timeoutValue: nil,
|
||||
timeoutSelected: false,
|
||||
displayGradient: false,
|
||||
bottomInset: 0.0
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 200.0)
|
||||
)
|
||||
|
||||
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelSize.height - 3.0), size: inputPanelSize)
|
||||
if let inputPanelView = self.inputPanel.view {
|
||||
if inputPanelView.superview == nil {
|
||||
self.addSubview(inputPanelView)
|
||||
}
|
||||
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -599,7 +599,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize))
|
||||
}
|
||||
|
||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 2.0
|
||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
||||
if case .story = component.style {
|
||||
let stickerButtonSize = self.stickerButton.update(
|
||||
transition: transition,
|
||||
@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if timeoutButtonView.superview == nil {
|
||||
self.addSubview(timeoutButtonView)
|
||||
}
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
|
@ -116,6 +116,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let backgroundEffectView: BlurredBackgroundView
|
||||
|
||||
private var contentUpdatedDisposable: Disposable?
|
||||
|
||||
@ -132,6 +133,9 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.backgroundLayer.backgroundColor = UIColor.black.cgColor
|
||||
self.backgroundLayer.zPosition = -1000.0
|
||||
|
||||
self.backgroundEffectView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.9), enableBlur: true)
|
||||
self.backgroundEffectView.layer.zPosition = -1001.0
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
@ -334,12 +338,12 @@ private final class StoryContainerScreenComponent: Component {
|
||||
|
||||
if subview is ItemSetView {
|
||||
if self.itemSetPanState == nil {
|
||||
if let result = subview.hitTest(point, with: event) {
|
||||
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
|
||||
if let result = subview.hitTest(self.convert(self.convert(point, to: subview), to: subview), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -351,6 +355,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
func animateIn() {
|
||||
if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil {
|
||||
self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
|
||||
if let transitionIn = self.component?.transitionIn, 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 {
|
||||
@ -372,6 +377,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
if 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, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) {
|
||||
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0)
|
||||
transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0)
|
||||
|
||||
let transitionOutCompleted = transitionOut.completed
|
||||
itemSetComponentView.animateOut(transitionOut: transitionOut, completion: {
|
||||
@ -450,6 +456,17 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.state = state
|
||||
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
self.backgroundLayer.isHidden = true
|
||||
self.backgroundEffectView.update(size: availableSize, transition: transition.containedViewLayoutTransition)
|
||||
self.insertSubview(self.backgroundEffectView, at: 0)
|
||||
|
||||
} else {
|
||||
self.backgroundLayer.isHidden = false
|
||||
self.backgroundEffectView.removeFromSuperview()
|
||||
}
|
||||
|
||||
var isProgressPaused = false
|
||||
if self.itemSetPanState != nil {
|
||||
@ -526,6 +543,16 @@ private final class StoryContainerScreenComponent: Component {
|
||||
self.visibleItemSetViews[slice.peer.id] = itemSetView
|
||||
}
|
||||
|
||||
let itemContainerSize: CGSize
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
let availableHeight = availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0
|
||||
let mediaHeight = min(1040.0, availableHeight - 90.0)
|
||||
let mediaWidth = floorToScreenPixels(mediaHeight * 0.5625)
|
||||
itemContainerSize = CGSize(width: mediaWidth, height: availableHeight)
|
||||
} else {
|
||||
itemContainerSize = availableSize
|
||||
}
|
||||
|
||||
let _ = itemSetView.view.update(
|
||||
transition: itemSetTransition,
|
||||
component: AnyComponent(StoryItemSetContainerComponent(
|
||||
@ -614,14 +641,14 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
containerSize: itemContainerSize
|
||||
)
|
||||
|
||||
if i == focusedIndex {
|
||||
contentDerivedBottomInset = itemSetView.externalState.derivedBottomInset
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(), size: availableSize)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - itemContainerSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - itemContainerSize.height) / 2.0)), size: itemContainerSize)
|
||||
if let itemSetComponentView = itemSetView.view.view {
|
||||
if itemSetView.superview == nil {
|
||||
self.addSubview(itemSetView)
|
||||
@ -741,7 +768,7 @@ private final class StoryContainerScreenComponent: Component {
|
||||
alphaFraction *= 1.3
|
||||
alphaFraction = max(-1.0, min(1.0, alphaFraction))
|
||||
alphaFraction = abs(alphaFraction)
|
||||
|
||||
|
||||
itemSetTransition.setAlpha(layer: itemSetView.tintLayer, alpha: alphaFraction)
|
||||
}
|
||||
}
|
||||
|
@ -550,7 +550,7 @@ final class StoryItemSetContainerSendMessage {
|
||||
guard let attachmentButtonView = inputPanelView.getAttachmentButtonView() else {
|
||||
return nil
|
||||
}
|
||||
return attachmentButtonView.convert(attachmentButtonView.bounds, to: view)
|
||||
return attachmentButtonView.convert(attachmentButtonView.bounds, to: nil)
|
||||
}
|
||||
attachmentController.requestController = { [weak self, weak view, weak attachmentController] type, completion in
|
||||
guard let self, let view, let component = view.component else {
|
||||
|
@ -23,6 +23,7 @@ public final class StoryPeerListComponent: Component {
|
||||
public let strings: PresentationStrings
|
||||
public let storySubscriptions: EngineStorySubscriptions?
|
||||
public let collapseFraction: CGFloat
|
||||
public let uploadProgress: Float?
|
||||
public let peerAction: (EnginePeer?) -> Void
|
||||
|
||||
public init(
|
||||
@ -32,6 +33,7 @@ public final class StoryPeerListComponent: Component {
|
||||
strings: PresentationStrings,
|
||||
storySubscriptions: EngineStorySubscriptions?,
|
||||
collapseFraction: CGFloat,
|
||||
uploadProgress: Float?,
|
||||
peerAction: @escaping (EnginePeer?) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
@ -40,6 +42,7 @@ public final class StoryPeerListComponent: Component {
|
||||
self.strings = strings
|
||||
self.storySubscriptions = storySubscriptions
|
||||
self.collapseFraction = collapseFraction
|
||||
self.uploadProgress = uploadProgress
|
||||
self.peerAction = peerAction
|
||||
}
|
||||
|
||||
@ -59,6 +62,9 @@ public final class StoryPeerListComponent: Component {
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.uploadProgress != rhs.uploadProgress {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -255,16 +261,14 @@ public final class StoryPeerListComponent: Component {
|
||||
hasUnseen = itemSet.hasUnseen
|
||||
|
||||
var hasItems = true
|
||||
var itemProgress: CGFloat?
|
||||
var itemProgress: Float?
|
||||
if peer.id == component.context.account.peerId {
|
||||
itemProgress = nil
|
||||
if let storySubscriptions = component.storySubscriptions, let accountItem = storySubscriptions.accountItem {
|
||||
hasItems = accountItem.storyCount != 0
|
||||
} else {
|
||||
hasItems = false
|
||||
}
|
||||
//itemProgress = component.state?.uploadProgress
|
||||
//itemProgress = 0.0
|
||||
itemProgress = component.uploadProgress
|
||||
}
|
||||
|
||||
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedContentOrigin + CGFloat(i - collapseStartIndex) * collapsedItemDistance, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
|
||||
|
@ -76,6 +76,7 @@ private final class StoryProgressLayer: SimpleShapeLayer {
|
||||
private struct Params: Equatable {
|
||||
var size: CGSize
|
||||
var lineWidth: CGFloat
|
||||
var progress: Float
|
||||
}
|
||||
|
||||
private var currentParams: Params?
|
||||
@ -100,10 +101,16 @@ private final class StoryProgressLayer: SimpleShapeLayer {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize, lineWidth: CGFloat) {
|
||||
func reset() {
|
||||
self.currentParams = nil
|
||||
self.path = nil
|
||||
}
|
||||
|
||||
func update(size: CGSize, lineWidth: CGFloat, progress: Float, transition: Transition) {
|
||||
let params = Params(
|
||||
size: size,
|
||||
lineWidth: lineWidth
|
||||
lineWidth: lineWidth,
|
||||
progress: progress
|
||||
)
|
||||
if self.currentParams == params {
|
||||
return
|
||||
@ -112,10 +119,13 @@ private final class StoryProgressLayer: SimpleShapeLayer {
|
||||
|
||||
let lineWidth: CGFloat = 2.0
|
||||
|
||||
let path = CGMutablePath()
|
||||
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: size.width * 0.5 - lineWidth * 0.5, startAngle: 0.0, endAngle: CGFloat.pi * 0.25, clockwise: false)
|
||||
if self.path == nil {
|
||||
let path = CGMutablePath()
|
||||
path.addEllipse(in: CGRect(origin: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
|
||||
self.path = path
|
||||
}
|
||||
|
||||
self.path = path
|
||||
transition.setShapeLayerStrokeEnd(layer: self, strokeEnd: CGFloat(progress))
|
||||
|
||||
if self.animation(forKey: "rotation") == nil {
|
||||
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
@ -136,7 +146,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
public let peer: EnginePeer
|
||||
public let hasUnseen: Bool
|
||||
public let hasItems: Bool
|
||||
public let progress: CGFloat?
|
||||
public let progress: Float?
|
||||
public let collapseFraction: CGFloat
|
||||
public let collapsedWidth: CGFloat
|
||||
public let leftNeighborDistance: CGFloat?
|
||||
@ -150,7 +160,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
peer: EnginePeer,
|
||||
hasUnseen: Bool,
|
||||
hasItems: Bool,
|
||||
progress: CGFloat?,
|
||||
progress: Float?,
|
||||
collapseFraction: CGFloat,
|
||||
collapsedWidth: CGFloat,
|
||||
leftNeighborDistance: CGFloat?,
|
||||
@ -287,6 +297,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
let hadProgress = self.component?.progress != nil
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.componentState = state
|
||||
|
||||
@ -429,6 +440,19 @@ public final class StoryPeerListItemComponent: Component {
|
||||
} else {
|
||||
titleString = component.peer.compactDisplayTitle
|
||||
}
|
||||
|
||||
var titleTransition = transition
|
||||
if previousComponent?.progress != nil && component.progress == nil {
|
||||
if let titleView = self.title.view, let snapshotView = titleView.snapshotContentTree() {
|
||||
titleView.superview?.addSubview(snapshotView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
titleTransition = .immediate
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: titleString, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||
@ -442,13 +466,13 @@ public final class StoryPeerListItemComponent: Component {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
titleTransition.setPosition(view: titleView, position: titleFrame.origin)
|
||||
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
transition.setScale(view: titleView, scale: effectiveScale)
|
||||
transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction)
|
||||
}
|
||||
|
||||
if component.progress != nil {
|
||||
if let progress = component.progress {
|
||||
var progressTransition = transition
|
||||
let progressLayer: StoryProgressLayer
|
||||
if let current = self.progressLayer {
|
||||
@ -461,7 +485,7 @@ public final class StoryPeerListItemComponent: Component {
|
||||
}
|
||||
let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size)
|
||||
progressTransition.setFrame(layer: progressLayer, frame: progressFrame)
|
||||
progressLayer.update(size: progressFrame.size, lineWidth: 4.0)
|
||||
progressLayer.update(size: progressFrame.size, lineWidth: 4.0, progress: progress, transition: transition)
|
||||
|
||||
self.indicatorShapeLayer.opacity = 0.0
|
||||
} else {
|
||||
@ -470,9 +494,11 @@ public final class StoryPeerListItemComponent: Component {
|
||||
if let progressLayer = self.progressLayer {
|
||||
self.progressLayer = nil
|
||||
if transition.animation.isImmediate {
|
||||
progressLayer.reset()
|
||||
progressLayer.removeFromSuperlayer()
|
||||
} else {
|
||||
progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in
|
||||
progressLayer?.reset()
|
||||
progressLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
|
@ -334,10 +334,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak self] mediaResult, commit, privacy in
|
||||
}, completion: { [weak self] mediaResult, privacy, commit in
|
||||
guard let self else {
|
||||
dismissCameraImpl?()
|
||||
commit()
|
||||
commit({})
|
||||
return
|
||||
}
|
||||
|
||||
@ -347,11 +347,26 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, _):
|
||||
let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start()
|
||||
Queue.mainQueue().after(0.3, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
case let .story(storyPrivacy, pin):
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
chatListController.updateStoryUploadProgress(progress)
|
||||
case .completed:
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({ [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
}
|
||||
case let .message(peerIds, timeout):
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
@ -400,6 +415,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
account: self.context.account,
|
||||
peerIds: peerIds, threadIds: [:],
|
||||
messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start()
|
||||
|
||||
commit({})
|
||||
}
|
||||
}
|
||||
case let .video(content, _, values, duration, dimensions, caption):
|
||||
@ -418,22 +435,34 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(localIdentifier):
|
||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||
}
|
||||
if case let .story(storyPrivacy, _) = privacy {
|
||||
let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start()
|
||||
Queue.mainQueue().after(0.3, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
if case let .story(storyPrivacy, pin) = privacy {
|
||||
chatListController.updateStoryUploadProgress(0.0)
|
||||
let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy)
|
||||
|> deliverOnMainQueue).start(next: { [weak chatListController] result in
|
||||
if let chatListController {
|
||||
switch result {
|
||||
case let .progress(progress):
|
||||
chatListController.updateStoryUploadProgress(progress)
|
||||
case .completed:
|
||||
Queue.mainQueue().after(0.2) {
|
||||
chatListController.updateStoryUploadProgress(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Queue.mainQueue().justDispatch {
|
||||
commit({ [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
commit({})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismissCameraImpl?()
|
||||
Queue.mainQueue().after(0.1) {
|
||||
commit()
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.cancelled = { showDraftTooltip in
|
||||
|
@ -143,7 +143,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) {
|
||||
init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) {
|
||||
self.tooltipStyle = style
|
||||
self.icon = icon
|
||||
self.customContentNode = customContentNode
|
||||
@ -286,7 +286,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
|
||||
if case let .point(_, arrowPosition) = location, case .right = arrowPosition {
|
||||
self.backgroundClipNode.cornerRadius = 8.5
|
||||
} else {
|
||||
self.backgroundClipNode.cornerRadius = 12.5
|
||||
self.backgroundClipNode.cornerRadius = cornerRadius ?? 12.5
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.backgroundClipNode.layer.cornerCurve = .continuous
|
||||
@ -736,6 +736,7 @@ public final class TooltipScreen: ViewController {
|
||||
}
|
||||
private let displayDuration: DisplayDuration
|
||||
private let inset: CGFloat
|
||||
private let cornerRadius: CGFloat?
|
||||
private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch
|
||||
private let openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?
|
||||
|
||||
@ -764,6 +765,7 @@ public final class TooltipScreen: ViewController {
|
||||
location: TooltipScreen.Location,
|
||||
displayDuration: DisplayDuration = .default,
|
||||
inset: CGFloat = 13.0,
|
||||
cornerRadius: CGFloat? = nil,
|
||||
shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch,
|
||||
openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil
|
||||
) {
|
||||
@ -777,6 +779,7 @@ public final class TooltipScreen: ViewController {
|
||||
self.location = location
|
||||
self.displayDuration = displayDuration
|
||||
self.inset = inset
|
||||
self.cornerRadius = cornerRadius
|
||||
self.shouldDismissOnTouch = shouldDismissOnTouch
|
||||
self.openActiveTextItem = openActiveTextItem
|
||||
|
||||
@ -836,7 +839,7 @@ public final class TooltipScreen: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = TooltipScreenNode(account: self.account, sharedContext: self.sharedContext, text: self.text, textEntities: self.textEntities, style: self.style, icon: self.icon, customContentNode: self.customContentNode, location: self.location, displayDuration: self.displayDuration, inset: self.inset, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
|
||||
self.displayNode = TooltipScreenNode(account: self.account, sharedContext: self.sharedContext, text: self.text, textEntities: self.textEntities, style: self.style, icon: self.icon, customContentNode: self.customContentNode, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user