Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-06-03 01:19:23 +04:00
parent 3df2d3cad5
commit ab69b9e982
48 changed files with 2154 additions and 557 deletions

View File

@ -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() {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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() {

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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()
//

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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> {

View File

@ -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))

View File

@ -214,7 +214,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
self.colorPixelFormat = .bgra8Unorm
self.framebufferOnly = true
//self.presentsWithTransaction = true
self.isPaused = true
self.delegate = self

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -17,4 +17,6 @@ public protocol DrawingEntity: AnyObject {
var renderImage: UIImage? { get set }
var renderSubEntities: [DrawingEntity]? { get set }
func isEqual(to other: DrawingEntity) -> Bool
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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))

View File

@ -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()
}
}

View File

@ -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? {

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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))

View File

@ -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()
})
}

View File

@ -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

View File

@ -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
}