mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
5fe2e3f69b
commit
b5c35cd8e7
@ -8449,7 +8449,11 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"UserInfo.SuggestPhoto" = "Suggest Photo for %@";
|
"UserInfo.SuggestPhoto" = "Suggest Photo for %@";
|
||||||
"UserInfo.SetCustomPhoto" = "Set Photo for %@";
|
"UserInfo.SetCustomPhoto" = "Set Photo for %@";
|
||||||
|
"UserInfo.ChangeCustomPhoto" = "Change Photo for %@";
|
||||||
"UserInfo.ResetCustomPhoto" = "Reset to Original Photo";
|
"UserInfo.ResetCustomPhoto" = "Reset to Original Photo";
|
||||||
|
"UserInfo.ResetCustomVideo" = "Reset to Original Video";
|
||||||
|
"UserInfo.RemoveCustomPhoto" = "Remove Photo";
|
||||||
|
"UserInfo.RemoveCustomVideo" = "Remove Video";
|
||||||
"UserInfo.CustomPhotoInfo" = "You can replace %@’s photo with another photo that only you will see.";
|
"UserInfo.CustomPhotoInfo" = "You can replace %@’s photo with another photo that only you will see.";
|
||||||
|
|
||||||
"UserInfo.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?";
|
"UserInfo.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?";
|
||||||
@ -8538,3 +8542,20 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"Attachment.EnableSpoiler" = "Hide With Spoiler";
|
"Attachment.EnableSpoiler" = "Hide With Spoiler";
|
||||||
"Attachment.DisableSpoiler" = "Disable Spoiler";
|
"Attachment.DisableSpoiler" = "Disable Spoiler";
|
||||||
|
|
||||||
|
"ProfilePhoto.PublicPhoto" = "public photo";
|
||||||
|
"ProfilePhoto.PublicVideo" = "public video";
|
||||||
|
|
||||||
|
"Paint.Draw" = "Draw";
|
||||||
|
"Paint.Sticker" = "Sticker";
|
||||||
|
"Paint.Text" = "Text";
|
||||||
|
"Paint.ZoomOut" = "Zoom Out";
|
||||||
|
|
||||||
|
"Paint.Rectangle" = "Rectangle";
|
||||||
|
"Paint.Ellipse" = "Ellipse";
|
||||||
|
"Paint.Bubble" = "Bubble";
|
||||||
|
"Paint.Star" = "Star";
|
||||||
|
"Paint.Arrow" = "Arrow";
|
||||||
|
|
||||||
|
"Paint.MoveForward" = "Move Forward";
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
public let inputHeight: CGFloat
|
public let inputHeight: CGFloat
|
||||||
public let metrics: LayoutMetrics
|
public let metrics: LayoutMetrics
|
||||||
public let deviceMetrics: DeviceMetrics
|
public let deviceMetrics: DeviceMetrics
|
||||||
|
public let orientation: UIInterfaceOrientation?
|
||||||
public let isVisible: Bool
|
public let isVisible: Bool
|
||||||
public let theme: PresentationTheme
|
public let theme: PresentationTheme
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
@ -40,6 +41,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
inputHeight: CGFloat,
|
inputHeight: CGFloat,
|
||||||
metrics: LayoutMetrics,
|
metrics: LayoutMetrics,
|
||||||
deviceMetrics: DeviceMetrics,
|
deviceMetrics: DeviceMetrics,
|
||||||
|
orientation: UIInterfaceOrientation? = nil,
|
||||||
isVisible: Bool,
|
isVisible: Bool,
|
||||||
theme: PresentationTheme,
|
theme: PresentationTheme,
|
||||||
strings: PresentationStrings,
|
strings: PresentationStrings,
|
||||||
@ -52,6 +54,7 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
self.inputHeight = inputHeight
|
self.inputHeight = inputHeight
|
||||||
self.metrics = metrics
|
self.metrics = metrics
|
||||||
self.deviceMetrics = deviceMetrics
|
self.deviceMetrics = deviceMetrics
|
||||||
|
self.orientation = orientation
|
||||||
self.isVisible = isVisible
|
self.isVisible = isVisible
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -82,6 +85,9 @@ open class ViewControllerComponentContainer: ViewController {
|
|||||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.orientation != rhs.orientation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.isVisible != rhs.isVisible {
|
if lhs.isVisible != rhs.isVisible {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -276,6 +276,18 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
panGestureRecognizer.delegate = self
|
panGestureRecognizer.delegate = self
|
||||||
self.addGestureRecognizer(panGestureRecognizer)
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
self.panGestureRecognizer = 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.onSnapToXAxis(snapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -296,6 +308,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let snapTool = DrawingEntitySnapTool()
|
||||||
|
|
||||||
private var currentHandle: CALayer?
|
private var currentHandle: CALayer?
|
||||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||||
@ -305,6 +319,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
|
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||||
|
|
||||||
if let sublayers = self.layer.sublayers {
|
if let sublayers = self.layer.sublayers {
|
||||||
for layer in sublayers {
|
for layer in sublayers {
|
||||||
if layer.frame.contains(location) {
|
if layer.frame.contains(location) {
|
||||||
@ -316,6 +332,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
self.currentHandle = self.layer
|
self.currentHandle = self.layer
|
||||||
case .changed:
|
case .changed:
|
||||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||||
|
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||||
|
|
||||||
var updatedSize = entity.size
|
var updatedSize = entity.size
|
||||||
var updatedPosition = entity.position
|
var updatedPosition = entity.position
|
||||||
@ -358,6 +375,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
} else if self.currentHandle === self.layer {
|
} else if self.currentHandle === self.layer {
|
||||||
updatedPosition.x += delta.x
|
updatedPosition.x += delta.x
|
||||||
updatedPosition.y += delta.y
|
updatedPosition.y += delta.y
|
||||||
|
|
||||||
|
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.size = updatedSize
|
entity.size = updatedSize
|
||||||
@ -367,7 +386,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
|||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
case .ended:
|
case .ended:
|
||||||
break
|
self.snapTool.reset()
|
||||||
|
case .cancelled:
|
||||||
|
self.snapTool.reset()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Display
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
@ -127,10 +128,19 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||||
private(set) var selectedEntityView: DrawingEntityView?
|
private(set) var selectedEntityView: DrawingEntityView?
|
||||||
|
|
||||||
|
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
|
||||||
|
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
|
||||||
public var hasSelectionChanged: (Bool) -> Void = { _ in }
|
public var hasSelectionChanged: (Bool) -> Void = { _ in }
|
||||||
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
|
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
|
||||||
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
|
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
|
||||||
|
|
||||||
|
var entityAdded: (DrawingEntity) -> Void = { _ in }
|
||||||
|
var entityRemoved: (DrawingEntity) -> Void = { _ in }
|
||||||
|
|
||||||
|
private let xAxisView = UIView()
|
||||||
|
private let yAxisView = UIView()
|
||||||
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
public init(context: AccountContext, size: CGSize) {
|
public init(context: AccountContext, size: CGSize) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.size = size
|
self.size = size
|
||||||
@ -140,12 +150,36 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||||
self.addGestureRecognizer(tapGestureRecognizer)
|
self.addGestureRecognizer(tapGestureRecognizer)
|
||||||
self.tapGestureRecognizer = tapGestureRecognizer
|
self.tapGestureRecognizer = tapGestureRecognizer
|
||||||
|
|
||||||
|
self.xAxisView.alpha = 0.0
|
||||||
|
self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||||
|
self.xAxisView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.yAxisView.alpha = 0.0
|
||||||
|
self.yAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0)
|
||||||
|
self.yAxisView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.addSubview(self.xAxisView)
|
||||||
|
self.addSubview(self.yAxisView)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let point = self.getEntityCenterPosition()
|
||||||
|
self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, 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: 10.0))
|
||||||
|
self.yAxisView.center = point
|
||||||
|
self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation())
|
||||||
|
}
|
||||||
|
|
||||||
var entities: [DrawingEntity] {
|
var entities: [DrawingEntity] {
|
||||||
var entities: [DrawingEntity] = []
|
var entities: [DrawingEntity] = []
|
||||||
for case let view as DrawingEntityView in self.subviews {
|
for case let view as DrawingEntityView in self.subviews {
|
||||||
@ -160,7 +194,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
|
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
|
||||||
let entities = codableEntities.map { $0.entity }
|
let entities = codableEntities.map { $0.entity }
|
||||||
for entity in entities {
|
for entity in entities {
|
||||||
self.add(entity)
|
self.add(entity, announce: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +219,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
return entity.center.offsetBy(dx: offset.x, dy: offset.y)
|
return entity.center.offsetBy(dx: offset.x, dy: offset.y)
|
||||||
} else {
|
} else {
|
||||||
let minimalDistance: CGFloat = round(offsetLength * 0.5)
|
let minimalDistance: CGFloat = round(offsetLength * 0.5)
|
||||||
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) // place good here
|
var position = self.getEntityCenterPosition()
|
||||||
|
|
||||||
while true {
|
while true {
|
||||||
var occupied = false
|
var occupied = false
|
||||||
@ -214,8 +248,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
|
|
||||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||||
let center = self.startPosition(relativeTo: relativeTo)
|
let center = self.startPosition(relativeTo: relativeTo)
|
||||||
|
let rotation = self.getEntityInitialRotation()
|
||||||
|
|
||||||
if let shape = entity as? DrawingSimpleShapeEntity {
|
if let shape = entity as? DrawingSimpleShapeEntity {
|
||||||
shape.position = center
|
shape.position = center
|
||||||
|
shape.rotation = rotation
|
||||||
|
|
||||||
if setup {
|
if setup {
|
||||||
let size = self.newEntitySize()
|
let size = self.newEntitySize()
|
||||||
@ -237,12 +274,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
} else if let sticker = entity as? DrawingStickerEntity {
|
} else if let sticker = entity as? DrawingStickerEntity {
|
||||||
sticker.position = center
|
sticker.position = center
|
||||||
|
sticker.rotation = rotation
|
||||||
if setup {
|
if setup {
|
||||||
sticker.referenceDrawingSize = self.size
|
sticker.referenceDrawingSize = self.size
|
||||||
sticker.scale = 1.0
|
sticker.scale = 1.0
|
||||||
}
|
}
|
||||||
} else if let bubble = entity as? DrawingBubbleEntity {
|
} else if let bubble = entity as? DrawingBubbleEntity {
|
||||||
bubble.position = center
|
bubble.position = center
|
||||||
|
bubble.rotation = rotation
|
||||||
if setup {
|
if setup {
|
||||||
let size = self.newEntitySize()
|
let size = self.newEntitySize()
|
||||||
bubble.referenceDrawingSize = self.size
|
bubble.referenceDrawingSize = self.size
|
||||||
@ -251,6 +290,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
} else if let text = entity as? DrawingTextEntity {
|
} else if let text = entity as? DrawingTextEntity {
|
||||||
text.position = center
|
text.position = center
|
||||||
|
text.rotation = rotation
|
||||||
if setup {
|
if setup {
|
||||||
text.referenceDrawingSize = self.size
|
text.referenceDrawingSize = self.size
|
||||||
text.width = floor(self.size.width * 0.9)
|
text.width = floor(self.size.width * 0.9)
|
||||||
@ -260,11 +300,45 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add(_ entity: DrawingEntity) -> DrawingEntityView {
|
func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
|
||||||
let view = entity.makeView(context: self.context)
|
let view = entity.makeView(context: self.context)
|
||||||
view.containerView = self
|
view.containerView = self
|
||||||
|
|
||||||
|
view.onSnapToXAxis = { [weak self] snappedToX in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||||
|
if snappedToX {
|
||||||
|
if strongSelf.xAxisView.alpha < 1.0 {
|
||||||
|
strongSelf.hapticFeedback.impact(.light)
|
||||||
|
}
|
||||||
|
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 1.0)
|
||||||
|
} else {
|
||||||
|
transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.onSnapToYAxis = { [weak self] snappedToY in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||||
|
if snappedToY {
|
||||||
|
if strongSelf.yAxisView.alpha < 1.0 {
|
||||||
|
strongSelf.hapticFeedback.impact(.light)
|
||||||
|
}
|
||||||
|
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 1.0)
|
||||||
|
} else {
|
||||||
|
transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
view.update()
|
view.update()
|
||||||
self.addSubview(view)
|
self.addSubview(view)
|
||||||
|
|
||||||
|
if announce {
|
||||||
|
self.entityAdded(entity)
|
||||||
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,15 +353,35 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
return newEntity
|
return newEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(uuid: UUID) {
|
func remove(uuid: UUID, animated: Bool = false, announce: Bool = true) {
|
||||||
if let view = self.getView(for: uuid) {
|
if let view = self.getView(for: uuid) {
|
||||||
if self.selectedEntityView === view {
|
if self.selectedEntityView === view {
|
||||||
self.selectedEntityView?.removeFromSuperview()
|
|
||||||
self.selectedEntityView = nil
|
self.selectedEntityView = nil
|
||||||
self.selectionChanged(nil)
|
self.selectionChanged(nil)
|
||||||
self.hasSelectionChanged(false)
|
self.hasSelectionChanged(false)
|
||||||
}
|
}
|
||||||
view.removeFromSuperview()
|
if animated {
|
||||||
|
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in
|
||||||
|
view?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
if !(view.entity is DrawingVectorEntity) {
|
||||||
|
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
if let selectionView = view.selectionView {
|
||||||
|
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] _ in
|
||||||
|
selectionView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
if !(view.entity is DrawingVectorEntity) {
|
||||||
|
selectionView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
if announce {
|
||||||
|
self.entityRemoved(view.entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,6 +569,9 @@ public class DrawingEntityView: UIView {
|
|||||||
weak var selectionView: DrawingEntitySelectionView?
|
weak var selectionView: DrawingEntitySelectionView?
|
||||||
weak var containerView: DrawingEntitiesView?
|
weak var containerView: DrawingEntitiesView?
|
||||||
|
|
||||||
|
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||||
|
var onSnapToYAxis: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
init(context: AccountContext, entity: DrawingEntity) {
|
init(context: AccountContext, entity: DrawingEntity) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
@ -17,21 +17,50 @@ import ViewControllerComponent
|
|||||||
import ContextUI
|
import ContextUI
|
||||||
import ChatEntityKeyboardInputNode
|
import ChatEntityKeyboardInputNode
|
||||||
import EntityKeyboard
|
import EntityKeyboard
|
||||||
|
import TelegramUIPreferences
|
||||||
|
|
||||||
enum DrawingToolState: Equatable {
|
enum DrawingToolState: Equatable, Codable {
|
||||||
enum Key: CaseIterable {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case pen
|
case type
|
||||||
case arrow
|
case brushState
|
||||||
case marker
|
case eraserState
|
||||||
case neon
|
|
||||||
case eraser
|
|
||||||
case blur
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BrushState: Equatable {
|
enum Key: Int32, RawRepresentable, CaseIterable, Codable {
|
||||||
|
case pen = 0
|
||||||
|
case arrow = 1
|
||||||
|
case marker = 2
|
||||||
|
case neon = 3
|
||||||
|
case eraser = 4
|
||||||
|
case blur = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BrushState: Equatable, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case color
|
||||||
|
case size
|
||||||
|
}
|
||||||
|
|
||||||
let color: DrawingColor
|
let color: DrawingColor
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
|
init(color: DrawingColor, size: CGFloat) {
|
||||||
|
self.color = color
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||||
|
self.size = try container.decode(CGFloat.self, forKey: .size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.size, forKey: .size)
|
||||||
|
}
|
||||||
|
|
||||||
func withUpdatedColor(_ color: DrawingColor) -> BrushState {
|
func withUpdatedColor(_ color: DrawingColor) -> BrushState {
|
||||||
return BrushState(color: color, size: self.size)
|
return BrushState(color: color, size: self.size)
|
||||||
}
|
}
|
||||||
@ -41,9 +70,27 @@ enum DrawingToolState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EraserState: Equatable {
|
struct EraserState: Equatable, Codable {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case size
|
||||||
|
}
|
||||||
|
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
|
init(size: CGFloat) {
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.size = try container.decode(CGFloat.self, forKey: .size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self.size, forKey: .size)
|
||||||
|
}
|
||||||
|
|
||||||
func withUpdatedSize(_ size: CGFloat) -> EraserState {
|
func withUpdatedSize(_ size: CGFloat) -> EraserState {
|
||||||
return EraserState(size: size)
|
return EraserState(size: size)
|
||||||
}
|
}
|
||||||
@ -122,6 +169,53 @@ enum DrawingToolState: Equatable {
|
|||||||
return .blur
|
return .blur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let typeValue = try container.decode(Int32.self, forKey: .type)
|
||||||
|
if let type = DrawingToolState.Key(rawValue: typeValue) {
|
||||||
|
switch type {
|
||||||
|
case .pen:
|
||||||
|
self = .pen(try container.decode(BrushState.self, forKey: .brushState))
|
||||||
|
case .arrow:
|
||||||
|
self = .arrow(try container.decode(BrushState.self, forKey: .brushState))
|
||||||
|
case .marker:
|
||||||
|
self = .marker(try container.decode(BrushState.self, forKey: .brushState))
|
||||||
|
case .neon:
|
||||||
|
self = .neon(try container.decode(BrushState.self, forKey: .brushState))
|
||||||
|
case .eraser:
|
||||||
|
self = .eraser(try container.decode(EraserState.self, forKey: .eraserState))
|
||||||
|
case .blur:
|
||||||
|
self = .blur(try container.decode(EraserState.self, forKey: .eraserState))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self = .pen(BrushState(color: DrawingColor(rgb: 0x000000), size: 0.5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
switch self {
|
||||||
|
case let .pen(state):
|
||||||
|
try container.encode(DrawingToolState.Key.pen.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .brushState)
|
||||||
|
case let .arrow(state):
|
||||||
|
try container.encode(DrawingToolState.Key.arrow.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .brushState)
|
||||||
|
case let .marker(state):
|
||||||
|
try container.encode(DrawingToolState.Key.marker.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .brushState)
|
||||||
|
case let .neon(state):
|
||||||
|
try container.encode(DrawingToolState.Key.neon.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .brushState)
|
||||||
|
case let .eraser(state):
|
||||||
|
try container.encode(DrawingToolState.Key.eraser.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .eraserState)
|
||||||
|
case let .blur(state):
|
||||||
|
try container.encode(DrawingToolState.Key.blur.rawValue, forKey: .type)
|
||||||
|
try container.encode(state, forKey: .eraserState)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DrawingState: Equatable {
|
struct DrawingState: Equatable {
|
||||||
@ -148,6 +242,13 @@ struct DrawingState: Equatable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withUpdatedTools(_ tools: [DrawingToolState]) -> DrawingState {
|
||||||
|
return DrawingState(
|
||||||
|
selectedTool: self.selectedTool,
|
||||||
|
tools: tools
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func withUpdatedColor(_ color: DrawingColor) -> DrawingState {
|
func withUpdatedColor(_ color: DrawingColor) -> DrawingState {
|
||||||
var tools = self.tools
|
var tools = self.tools
|
||||||
if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) {
|
if let index = tools.firstIndex(where: { $0.key == self.selectedTool }) {
|
||||||
@ -191,6 +292,36 @@ struct DrawingState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class DrawingSettings: Codable, Equatable {
|
||||||
|
let tools: [DrawingToolState]
|
||||||
|
|
||||||
|
init(tools: [DrawingToolState]) {
|
||||||
|
self.tools = tools
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
|
if let data = try container.decodeIfPresent(Data.self, forKey: "tools"), let tools = try? JSONDecoder().decode([DrawingToolState].self, from: data) {
|
||||||
|
self.tools = tools
|
||||||
|
} else {
|
||||||
|
self.tools = DrawingState.initial.tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||||
|
|
||||||
|
if let data = try? JSONEncoder().encode(self.tools) {
|
||||||
|
try container.encode(data, forKey: "tools")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: DrawingSettings, rhs: DrawingSettings) -> Bool {
|
||||||
|
return lhs.tools == rhs.tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ReferenceContentSource: ContextReferenceContentSource {
|
private final class ReferenceContentSource: ContextReferenceContentSource {
|
||||||
private let sourceView: UIView
|
private let sourceView: UIView
|
||||||
|
|
||||||
@ -217,6 +348,7 @@ private let toolsTag = GenericComponentViewTag()
|
|||||||
private let modeTag = GenericComponentViewTag()
|
private let modeTag = GenericComponentViewTag()
|
||||||
private let flipButtonTag = GenericComponentViewTag()
|
private let flipButtonTag = GenericComponentViewTag()
|
||||||
private let fillButtonTag = GenericComponentViewTag()
|
private let fillButtonTag = GenericComponentViewTag()
|
||||||
|
private let zoomOutButtonTag = GenericComponentViewTag()
|
||||||
private let textSettingsTag = GenericComponentViewTag()
|
private let textSettingsTag = GenericComponentViewTag()
|
||||||
private let sizeSliderTag = GenericComponentViewTag()
|
private let sizeSliderTag = GenericComponentViewTag()
|
||||||
private let color1Tag = GenericComponentViewTag()
|
private let color1Tag = GenericComponentViewTag()
|
||||||
@ -432,6 +564,35 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
stickerPickerInputData.set(signal)
|
stickerPickerInputData.set(signal)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.loadToolState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadToolState() {
|
||||||
|
let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.drawingSettings])
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let drawingSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.drawingSettings]?.get(DrawingSettings.self) {
|
||||||
|
strongSelf.drawingState = strongSelf.drawingState.withUpdatedTools(drawingSettings.tools)
|
||||||
|
strongSelf.currentColor = strongSelf.drawingState.currentToolState.color ?? strongSelf.currentColor
|
||||||
|
strongSelf.updated(transition: .immediate)
|
||||||
|
strongSelf.updateToolState.invoke(strongSelf.drawingState.currentToolState)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveToolState() {
|
||||||
|
let tools = self.drawingState.tools
|
||||||
|
let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Void in
|
||||||
|
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.drawingSettings, { _ in
|
||||||
|
return PreferencesEntry(DrawingSettings(tools: tools))
|
||||||
|
})
|
||||||
|
}).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentToolState: DrawingToolState {
|
private var currentToolState: DrawingToolState {
|
||||||
@ -508,10 +669,12 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func presentShapePicker(_ sourceView: UIView) {
|
func presentShapePicker(_ sourceView: UIView) {
|
||||||
|
let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||||
|
|
||||||
let items: [ContextMenuItem] = [
|
let items: [ContextMenuItem] = [
|
||||||
.action(
|
.action(
|
||||||
ContextMenuActionItem(
|
ContextMenuActionItem(
|
||||||
text: "Rectangle",
|
text: strings.Paint_Rectangle,
|
||||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)},
|
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)},
|
||||||
action: { [weak self] f in
|
action: { [weak self] f in
|
||||||
f.dismissWithResult(.default)
|
f.dismissWithResult(.default)
|
||||||
@ -523,7 +686,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
),
|
),
|
||||||
.action(
|
.action(
|
||||||
ContextMenuActionItem(
|
ContextMenuActionItem(
|
||||||
text: "Ellipse",
|
text: strings.Paint_Ellipse,
|
||||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)},
|
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)},
|
||||||
action: { [weak self] f in
|
action: { [weak self] f in
|
||||||
f.dismissWithResult(.default)
|
f.dismissWithResult(.default)
|
||||||
@ -535,7 +698,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
),
|
),
|
||||||
.action(
|
.action(
|
||||||
ContextMenuActionItem(
|
ContextMenuActionItem(
|
||||||
text: "Bubble",
|
text: strings.Paint_Bubble,
|
||||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)},
|
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)},
|
||||||
action: { [weak self] f in
|
action: { [weak self] f in
|
||||||
f.dismissWithResult(.default)
|
f.dismissWithResult(.default)
|
||||||
@ -547,7 +710,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
),
|
),
|
||||||
.action(
|
.action(
|
||||||
ContextMenuActionItem(
|
ContextMenuActionItem(
|
||||||
text: "Star",
|
text: strings.Paint_Star,
|
||||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)},
|
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)},
|
||||||
action: { [weak self] f in
|
action: { [weak self] f in
|
||||||
f.dismissWithResult(.default)
|
f.dismissWithResult(.default)
|
||||||
@ -559,7 +722,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
),
|
),
|
||||||
.action(
|
.action(
|
||||||
ContextMenuActionItem(
|
ContextMenuActionItem(
|
||||||
text: "Arrow",
|
text: strings.Paint_Arrow,
|
||||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)},
|
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)},
|
||||||
action: { [weak self] f in
|
action: { [weak self] f in
|
||||||
f.dismissWithResult(.default)
|
f.dismissWithResult(.default)
|
||||||
@ -669,13 +832,23 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
let state = context.state
|
let state = context.state
|
||||||
let controller = environment.controller
|
let controller = environment.controller
|
||||||
|
|
||||||
|
let strings = environment.strings
|
||||||
|
|
||||||
let previewBrushSize = component.previewBrushSize
|
let previewBrushSize = component.previewBrushSize
|
||||||
let performAction = component.performAction
|
let performAction = component.performAction
|
||||||
component.updateState.connect { [weak state] updatedState in
|
component.updateState.connect { [weak state] updatedState in
|
||||||
state?.updateDrawingState(updatedState)
|
state?.updateDrawingState(updatedState)
|
||||||
}
|
}
|
||||||
component.updateColor.connect { [weak state] color in
|
component.updateColor.connect { [weak state] color in
|
||||||
state?.updateColor(color)
|
if let state = state {
|
||||||
|
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
|
||||||
|
state.updateSelectedTool(.pen, update: false)
|
||||||
|
state.updateColor(color, animated: true)
|
||||||
|
} else {
|
||||||
|
state.updateColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
component.updateSelectedEntity.connect { [weak state] entity in
|
component.updateSelectedEntity.connect { [weak state] entity in
|
||||||
state?.updateSelectedEntity(entity)
|
state?.updateSelectedEntity(entity)
|
||||||
@ -1175,14 +1348,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
ZoomOutButtonContent(
|
ZoomOutButtonContent(
|
||||||
title: "Zoom Out",
|
title: strings.Paint_ZoomOut,
|
||||||
image: state.image(.zoomOut)
|
image: state.image(.zoomOut)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
performAction.invoke(.zoomOut)
|
performAction.invoke(.zoomOut)
|
||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
|
||||||
availableSize: CGSize(width: 120.0, height: 33.0),
|
availableSize: CGSize(width: 120.0, height: 33.0),
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
@ -1241,30 +1414,29 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
.opacity(isEditingText ? 0.0 : 1.0)
|
.opacity(isEditingText ? 0.0 : 1.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
if state.drawingViewState.canRedo && !isEditingText {
|
|
||||||
let redoButton = redoButton.update(
|
let redoButton = redoButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
Image(image: state.image(.redo))
|
Image(image: state.image(.redo))
|
||||||
),
|
),
|
||||||
action: {
|
action: {
|
||||||
performAction.invoke(.redo)
|
performAction.invoke(.redo)
|
||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
)
|
)
|
||||||
context.add(redoButton
|
context.add(redoButton
|
||||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||||
.appear(.default(scale: true, alpha: true))
|
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||||
.disappear(.default(scale: true, alpha: true))
|
.opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
let clearAllButton = clearAllButton.update(
|
let clearAllButton = clearAllButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
Text(text: "Clear All", font: Font.regular(17.0), color: .white)
|
Text(text: strings.Paint_Clear, font: Font.regular(17.0), color: .white)
|
||||||
),
|
),
|
||||||
isEnabled: state.drawingViewState.canClear,
|
isEnabled: state.drawingViewState.canClear,
|
||||||
action: {
|
action: {
|
||||||
@ -1327,6 +1499,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
color = nil
|
color = nil
|
||||||
} else if state.selectedEntity is DrawingStickerEntity {
|
} else if state.selectedEntity is DrawingStickerEntity {
|
||||||
color = nil
|
color = nil
|
||||||
|
} else if [.eraser, .blur].contains(state.drawingState.selectedTool) {
|
||||||
|
color = nil
|
||||||
} else {
|
} else {
|
||||||
color = state.currentColor
|
color = state.currentColor
|
||||||
}
|
}
|
||||||
@ -1413,7 +1587,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
Image(image: state.image(.done))
|
Image(image: state.image(.done))
|
||||||
),
|
),
|
||||||
action: {
|
action: { [weak state] in
|
||||||
|
state?.saveToolState()
|
||||||
apply.invoke(Void())
|
apply.invoke(Void())
|
||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
|
||||||
@ -1456,7 +1631,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let modeAndSize = modeAndSize.update(
|
let modeAndSize = modeAndSize.update(
|
||||||
component: ModeAndSizeComponent(
|
component: ModeAndSizeComponent(
|
||||||
values: ["Draw", "Sticker", "Text"],
|
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
|
||||||
sizeValue: selectedSize,
|
sizeValue: selectedSize,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -1500,7 +1675,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
animatingOut = true
|
animatingOut = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let deselectEntity = component.deselectEntity
|
|
||||||
let backButton = backButton.update(
|
let backButton = backButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
@ -1516,11 +1690,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
),
|
),
|
||||||
action: { [weak state] in
|
action: { [weak state] in
|
||||||
if let state = state {
|
if let state = state {
|
||||||
if let selectedEntity = state.selectedEntity, !(selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity) {
|
state.saveToolState()
|
||||||
deselectEntity.invoke(Void())
|
dismiss.invoke(Void())
|
||||||
} else {
|
|
||||||
dismiss.invoke(Void())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||||
@ -1559,7 +1730,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)?
|
||||||
|
|
||||||
private var _drawingView: DrawingView?
|
private var _drawingView: DrawingView?
|
||||||
var drawingView: DrawingView {
|
var drawingView: DrawingView {
|
||||||
@ -1655,6 +1826,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
if self._entitiesView == nil, let controller = self.controller {
|
if self._entitiesView == nil, let controller = self.controller {
|
||||||
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
|
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
|
||||||
self._drawingView?.entitiesView = self._entitiesView
|
self._drawingView?.entitiesView = self._entitiesView
|
||||||
|
self._entitiesView?.entityAdded = { [weak self] entity in
|
||||||
|
self?._drawingView?.onEntityAdded(entity)
|
||||||
|
}
|
||||||
|
self._entitiesView?.entityRemoved = { [weak self] entity in
|
||||||
|
self?._drawingView?.onEntityRemoved(entity)
|
||||||
|
}
|
||||||
let entitiesLayer = self.entitiesView.layer
|
let entitiesLayer = self.entitiesView.layer
|
||||||
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
|
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
|
||||||
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
|
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
|
||||||
@ -1699,7 +1876,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
var actions: [ContextMenuAction] = []
|
var actions: [ContextMenuAction] = []
|
||||||
actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in
|
actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_Delete, accessibilityLabel: strongSelf.presentationData.strings.Paint_Delete), action: { [weak self, weak entityView] in
|
||||||
if let strongSelf = self, let entityView = entityView {
|
if let strongSelf = self, let entityView = entityView {
|
||||||
strongSelf.entitiesView.remove(uuid: entityView.entity.uuid)
|
strongSelf.entitiesView.remove(uuid: entityView.entity.uuid, animated: true)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
if let entityView = entityView as? DrawingTextEntityView {
|
if let entityView = entityView as? DrawingTextEntityView {
|
||||||
@ -1711,7 +1888,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
if !isTopmost {
|
if !isTopmost {
|
||||||
actions.append(ContextMenuAction(content: .text(title: "Move Forward", accessibilityLabel: "Move Forward"), action: { [weak self, weak entityView] in
|
actions.append(ContextMenuAction(content: .text(title: strongSelf.presentationData.strings.Paint_MoveForward, accessibilityLabel: strongSelf.presentationData.strings.Paint_MoveForward), action: { [weak self, weak entityView] in
|
||||||
if let strongSelf = self, let entityView = entityView {
|
if let strongSelf = self, let entityView = entityView {
|
||||||
strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid)
|
strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid)
|
||||||
}
|
}
|
||||||
@ -1975,8 +2152,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func animateOut(completion: @escaping () -> Void) {
|
func animateOut(completion: @escaping () -> Void) {
|
||||||
if let layout = self.validLayout {
|
if let (layout, orientation) = self.validLayout {
|
||||||
self.containerLayoutUpdated(layout: layout, animateOut: true, transition: .easeInOut(duration: 0.2))
|
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: true, transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
|
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
|
||||||
@ -2012,6 +2189,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||||
}
|
}
|
||||||
|
if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) {
|
||||||
|
buttonView.alpha = 0.0
|
||||||
|
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||||
|
}
|
||||||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
||||||
}
|
}
|
||||||
@ -2052,12 +2234,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func containerLayoutUpdated(layout: ContainerViewLayout, animateOut: Bool = false, transition: Transition) {
|
func containerLayoutUpdated(layout: ContainerViewLayout, orientation: UIInterfaceOrientation?, animateOut: Bool = false, transition: Transition) {
|
||||||
guard let controller = self.controller else {
|
guard let controller = self.controller else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let isFirstTime = self.validLayout == nil
|
let isFirstTime = self.validLayout == nil
|
||||||
self.validLayout = layout
|
self.validLayout = (layout, orientation)
|
||||||
|
|
||||||
let environment = ViewControllerComponentContainer.Environment(
|
let environment = ViewControllerComponentContainer.Environment(
|
||||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||||
@ -2071,6 +2253,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
inputHeight: layout.inputHeight ?? 0.0,
|
inputHeight: layout.inputHeight ?? 0.0,
|
||||||
metrics: layout.metrics,
|
metrics: layout.metrics,
|
||||||
deviceMetrics: layout.deviceMetrics,
|
deviceMetrics: layout.deviceMetrics,
|
||||||
|
orientation: orientation,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
theme: self.presentationData.theme,
|
theme: self.presentationData.theme,
|
||||||
strings: self.presentationData.strings,
|
strings: self.presentationData.strings,
|
||||||
@ -2195,8 +2378,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
textEntity.style = nextStyle
|
textEntity.style = nextStyle
|
||||||
entityView.update()
|
entityView.update()
|
||||||
|
|
||||||
if let layout = strongSelf.validLayout {
|
if let (layout, orientation) = strongSelf.validLayout {
|
||||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleAlignment: { [weak self] in
|
toggleAlignment: { [weak self] in
|
||||||
@ -2215,8 +2398,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
textEntity.alignment = nextAlignment
|
textEntity.alignment = nextAlignment
|
||||||
entityView.update()
|
entityView.update()
|
||||||
|
|
||||||
if let layout = strongSelf.validLayout {
|
if let (layout, orientation) = strongSelf.validLayout {
|
||||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateFont: { [weak self] font in
|
updateFont: { [weak self] font in
|
||||||
@ -2226,8 +2409,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
textEntity.font = font.font
|
textEntity.font = font.font
|
||||||
entityView.update()
|
entityView.update()
|
||||||
|
|
||||||
if let layout = strongSelf.validLayout {
|
if let (layout, orientation) = strongSelf.validLayout {
|
||||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleKeyboard: { [weak self] in
|
toggleKeyboard: { [weak self] in
|
||||||
@ -2300,8 +2483,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
textView.becomeFirstResponder()
|
textView.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let layout = self.validLayout {
|
if let (layout, orientation) = self.validLayout {
|
||||||
self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate)
|
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2350,10 +2533,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
|
||||||
print()
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
override public func loadDisplayNode() {
|
||||||
self.displayNode = Node(controller: self, context: self.context)
|
self.displayNode = Node(controller: self, context: self.context)
|
||||||
|
|
||||||
@ -2404,7 +2583,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
image = finalImage
|
image = finalImage
|
||||||
}
|
}
|
||||||
|
|
||||||
return TGPaintingData(drawing: nil, entitiesData: self.entitiesView.entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities)
|
let entitiesData = self.entitiesView.entitiesData
|
||||||
|
|
||||||
|
var stickers: [Any] = []
|
||||||
|
for entity in self.entitiesView.entities {
|
||||||
|
if let sticker = entity as? DrawingStickerEntity {
|
||||||
|
let coder = PostboxEncoder()
|
||||||
|
coder.encodeRootObject(sticker.file)
|
||||||
|
stickers.append(coder.makeData())
|
||||||
|
} else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities {
|
||||||
|
for sticker in subEntities {
|
||||||
|
let coder = PostboxEncoder()
|
||||||
|
coder.encodeRootObject(sticker.file)
|
||||||
|
stickers.append(coder.makeData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TGPaintingData(drawing: nil, entitiesData: entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities, stickers: stickers)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func resultImage() -> UIImage! {
|
public func resultImage() -> UIImage! {
|
||||||
@ -2428,13 +2624,14 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
self.node.animateOut(completion: completion)
|
self.node.animateOut(completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var orientation: UIInterfaceOrientation?
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
super.containerLayoutUpdated(layout, transition: transition)
|
||||||
|
|
||||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition))
|
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, orientation: orientation, transition: Transition(transition))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, animated: Bool) {
|
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, animated: Bool) {
|
||||||
let layout = ContainerViewLayout(
|
let layout = ContainerViewLayout(
|
||||||
size: size,
|
size: size,
|
||||||
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
|
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
|
||||||
@ -2447,6 +2644,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
inputHeightIsInteractivellyChanging: false,
|
inputHeightIsInteractivellyChanging: false,
|
||||||
inVoiceOver: false
|
inVoiceOver: false
|
||||||
)
|
)
|
||||||
|
self.orientation = orientation
|
||||||
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +185,11 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView {
|
|||||||
return max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.05)
|
return max(10.0, max(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height) * 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate var minimumSize: CGSize {
|
||||||
|
let minSize = min(self.shapeEntity.referenceDrawingSize.width, self.shapeEntity.referenceDrawingSize.height)
|
||||||
|
return CGSize(width: minSize * 0.1, height: minSize * 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
let lineWidth = self.maxLineWidth * 0.5
|
let lineWidth = self.maxLineWidth * 0.5
|
||||||
let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth)
|
let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth)
|
||||||
@ -291,6 +296,18 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
|||||||
panGestureRecognizer.delegate = self
|
panGestureRecognizer.delegate = self
|
||||||
self.addGestureRecognizer(panGestureRecognizer)
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
self.panGestureRecognizer = 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.onSnapToXAxis(snapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -311,9 +328,11 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let snapTool = DrawingEntitySnapTool()
|
||||||
|
|
||||||
private var currentHandle: CALayer?
|
private var currentHandle: CALayer?
|
||||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
guard let entityView = self.entityView as? DrawingSimpleShapeEntityView, let entity = entityView.entity as? DrawingSimpleShapeEntity else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let isAspectLocked = [.star].contains(entity.shapeType)
|
let isAspectLocked = [.star].contains(entity.shapeType)
|
||||||
@ -321,6 +340,8 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
|||||||
|
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||||
|
|
||||||
if let sublayers = self.layer.sublayers {
|
if let sublayers = self.layer.sublayers {
|
||||||
for layer in sublayers {
|
for layer in sublayers {
|
||||||
if layer.frame.contains(location) {
|
if layer.frame.contains(location) {
|
||||||
@ -332,50 +353,54 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
|||||||
self.currentHandle = self.layer
|
self.currentHandle = self.layer
|
||||||
case .changed:
|
case .changed:
|
||||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||||
|
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||||
|
|
||||||
var updatedSize = entity.size
|
var updatedSize = entity.size
|
||||||
var updatedPosition = entity.position
|
var updatedPosition = entity.position
|
||||||
|
|
||||||
|
let minimumSize = entityView.minimumSize
|
||||||
|
|
||||||
if self.currentHandle === self.leftHandle {
|
if self.currentHandle === self.leftHandle {
|
||||||
let deltaX = delta.x * cos(entity.rotation)
|
let deltaX = delta.x * cos(entity.rotation)
|
||||||
let deltaY = delta.x * sin(entity.rotation)
|
let deltaY = delta.x * sin(entity.rotation)
|
||||||
|
|
||||||
updatedSize.width -= deltaX
|
updatedSize.width = max(minimumSize.width, updatedSize.width - deltaX)
|
||||||
updatedPosition.x -= deltaX * -0.5
|
updatedPosition.x -= deltaX * -0.5
|
||||||
updatedPosition.y -= deltaY * -0.5
|
updatedPosition.y -= deltaY * -0.5
|
||||||
|
|
||||||
if isAspectLocked {
|
if isAspectLocked {
|
||||||
updatedSize.height -= delta.x
|
updatedSize.height = updatedSize.width
|
||||||
}
|
}
|
||||||
} else if self.currentHandle === self.rightHandle {
|
} else if self.currentHandle === self.rightHandle {
|
||||||
let deltaX = delta.x * cos(entity.rotation)
|
let deltaX = delta.x * cos(entity.rotation)
|
||||||
let deltaY = delta.x * sin(entity.rotation)
|
let deltaY = delta.x * sin(entity.rotation)
|
||||||
|
|
||||||
updatedSize.width += deltaX
|
updatedSize.width = max(minimumSize.width, updatedSize.width + deltaX)
|
||||||
|
print(updatedSize.width)
|
||||||
updatedPosition.x += deltaX * 0.5
|
updatedPosition.x += deltaX * 0.5
|
||||||
updatedPosition.y += deltaY * 0.5
|
updatedPosition.y += deltaY * 0.5
|
||||||
if isAspectLocked {
|
if isAspectLocked {
|
||||||
updatedSize.height += delta.x
|
updatedSize.height = updatedSize.width
|
||||||
}
|
}
|
||||||
} else if self.currentHandle === self.topHandle {
|
} else if self.currentHandle === self.topHandle {
|
||||||
let deltaX = delta.y * sin(entity.rotation)
|
let deltaX = delta.y * sin(entity.rotation)
|
||||||
let deltaY = delta.y * cos(entity.rotation)
|
let deltaY = delta.y * cos(entity.rotation)
|
||||||
|
|
||||||
updatedSize.height -= deltaY
|
updatedSize.height = max(minimumSize.height, updatedSize.height - deltaY)
|
||||||
updatedPosition.x += deltaX * 0.5
|
updatedPosition.x += deltaX * 0.5
|
||||||
updatedPosition.y += deltaY * 0.5
|
updatedPosition.y += deltaY * 0.5
|
||||||
if isAspectLocked {
|
if isAspectLocked {
|
||||||
updatedSize.width -= delta.y
|
updatedSize.width = updatedSize.height
|
||||||
}
|
}
|
||||||
} else if self.currentHandle === self.bottomHandle {
|
} else if self.currentHandle === self.bottomHandle {
|
||||||
let deltaX = delta.y * sin(entity.rotation)
|
let deltaX = delta.y * sin(entity.rotation)
|
||||||
let deltaY = delta.y * cos(entity.rotation)
|
let deltaY = delta.y * cos(entity.rotation)
|
||||||
|
|
||||||
updatedSize.height += deltaY
|
updatedSize.height = max(minimumSize.height, updatedSize.height + deltaY)
|
||||||
updatedPosition.x += deltaX * 0.5
|
updatedPosition.x += deltaX * 0.5
|
||||||
updatedPosition.y += deltaY * 0.5
|
updatedPosition.y += deltaY * 0.5
|
||||||
if isAspectLocked {
|
if isAspectLocked {
|
||||||
updatedSize.width += delta.y
|
updatedSize.width = updatedSize.height
|
||||||
}
|
}
|
||||||
} else if self.currentHandle === self.topLeftHandle {
|
} else if self.currentHandle === self.topLeftHandle {
|
||||||
var delta = delta
|
var delta = delta
|
||||||
@ -416,15 +441,19 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
|||||||
} else if self.currentHandle === self.layer {
|
} else if self.currentHandle === self.layer {
|
||||||
updatedPosition.x += delta.x
|
updatedPosition.x += delta.x
|
||||||
updatedPosition.y += delta.y
|
updatedPosition.y += delta.y
|
||||||
|
|
||||||
|
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.size = updatedSize
|
entity.size = updatedSize
|
||||||
entity.position = updatedPosition
|
entity.position = updatedPosition
|
||||||
entityView.update()
|
entityView.update(animated: false)
|
||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
case .ended:
|
case .ended:
|
||||||
break
|
self.snapTool.reset()
|
||||||
|
case .cancelled:
|
||||||
|
self.snapTool.reset()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -340,6 +340,10 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
self.border.lineCap = .round
|
self.border.lineCap = .round
|
||||||
self.border.fillColor = UIColor.clear.cgColor
|
self.border.fillColor = UIColor.clear.cgColor
|
||||||
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
|
||||||
|
self.border.shadowColor = UIColor.black.cgColor
|
||||||
|
self.border.shadowRadius = 1.0
|
||||||
|
self.border.shadowOpacity = 0.5
|
||||||
|
self.border.shadowOffset = CGSize()
|
||||||
self.layer.addSublayer(self.border)
|
self.layer.addSublayer(self.border)
|
||||||
|
|
||||||
for handle in handles {
|
for handle in handles {
|
||||||
@ -356,6 +360,18 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
panGestureRecognizer.delegate = self
|
panGestureRecognizer.delegate = self
|
||||||
self.addGestureRecognizer(panGestureRecognizer)
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
self.panGestureRecognizer = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -376,6 +392,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let snapTool = DrawingEntitySnapTool()
|
||||||
|
|
||||||
private var currentHandle: CALayer?
|
private var currentHandle: CALayer?
|
||||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingStickerEntity else {
|
||||||
@ -383,8 +401,11 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
}
|
}
|
||||||
let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
|
|
||||||
|
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||||
|
|
||||||
if let sublayers = self.layer.sublayers {
|
if let sublayers = self.layer.sublayers {
|
||||||
for layer in sublayers {
|
for layer in sublayers {
|
||||||
if layer.frame.contains(location) {
|
if layer.frame.contains(location) {
|
||||||
@ -397,6 +418,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
case .changed:
|
case .changed:
|
||||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||||
|
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||||
|
|
||||||
var updatedPosition = entity.position
|
var updatedPosition = entity.position
|
||||||
var updatedScale = entity.scale
|
var updatedScale = entity.scale
|
||||||
@ -419,6 +441,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
} else if self.currentHandle === self.layer {
|
} else if self.currentHandle === self.layer {
|
||||||
updatedPosition.x += delta.x
|
updatedPosition.x += delta.x
|
||||||
updatedPosition.y += delta.y
|
updatedPosition.y += delta.y
|
||||||
|
|
||||||
|
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.position = updatedPosition
|
entity.position = updatedPosition
|
||||||
@ -428,7 +452,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
case .ended:
|
case .ended:
|
||||||
break
|
self.snapTool.reset()
|
||||||
|
case .cancelled:
|
||||||
|
self.snapTool.reset()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -494,8 +520,121 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
|||||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||||
|
|
||||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
|
||||||
|
let radius = (self.bounds.width - inset * 2.0) / 2.0
|
||||||
|
let circumference: CGFloat = 2.0 * .pi * radius
|
||||||
|
let count = 10
|
||||||
|
let relativeDashLength: CGFloat = 0.25
|
||||||
|
let dashLength = circumference / CGFloat(count)
|
||||||
|
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
|
||||||
|
|
||||||
self.border.lineWidth = 2.0 / self.scale
|
self.border.lineWidth = 2.0 / self.scale
|
||||||
self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath
|
self.border.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0))).cgPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DrawingEntitySnapTool {
|
||||||
|
private var xState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||||
|
private var yState: (skipped: CGFloat, waitForLeave: Bool)?
|
||||||
|
|
||||||
|
var onSnapXUpdated: (Bool) -> Void = { _ in }
|
||||||
|
var onSnapYUpdated: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
self.xState = nil
|
||||||
|
self.yState = nil
|
||||||
|
|
||||||
|
self.onSnapXUpdated(false)
|
||||||
|
self.onSnapYUpdated(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) {
|
||||||
|
self.xState = nil
|
||||||
|
self.yState = nil
|
||||||
|
|
||||||
|
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||||
|
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||||
|
|
||||||
|
if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() {
|
||||||
|
if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta {
|
||||||
|
self.xState = (0.0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta {
|
||||||
|
self.yState = (0.0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint {
|
||||||
|
var updatedPosition = updatedPosition
|
||||||
|
|
||||||
|
let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||||
|
let snapXVelocity: CGFloat = snapXDelta * 10.0
|
||||||
|
let snapXSkipTranslation: CGFloat = snapXDelta * 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 * 1.5 && updatedPosition.x < snapLocation.x + snapXDelta * 1.5 {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.xState = nil
|
||||||
|
}
|
||||||
|
} 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 {
|
||||||
|
self.xState = (0.0, false)
|
||||||
|
updatedPosition.x = snapLocation.x
|
||||||
|
self.onSnapXUpdated(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.xState = nil
|
||||||
|
self.onSnapXUpdated(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02
|
||||||
|
let snapYVelocity: CGFloat = snapYDelta * 10.0
|
||||||
|
let snapYSkipTranslation: CGFloat = snapYDelta * 2.0
|
||||||
|
|
||||||
|
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 * 1.5 && updatedPosition.y < snapLocation.y + snapYDelta * 1.5 {
|
||||||
|
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if updatedPosition.y > snapLocation.y - snapYDelta && updatedPosition.y < snapLocation.y + snapYDelta {
|
||||||
|
self.yState = (0.0, false)
|
||||||
|
updatedPosition.y = snapLocation.y
|
||||||
|
self.onSnapYUpdated(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.yState = nil
|
||||||
|
self.onSnapYUpdated(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -81,19 +81,7 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
case newYork
|
case newYork
|
||||||
case monospaced
|
case monospaced
|
||||||
case round
|
case round
|
||||||
|
case custom(String, String)
|
||||||
init(font: DrawingTextEntity.Font) {
|
|
||||||
switch font {
|
|
||||||
case .sanFrancisco:
|
|
||||||
self = .sanFrancisco
|
|
||||||
case .newYork:
|
|
||||||
self = .newYork
|
|
||||||
case .monospaced:
|
|
||||||
self = .monospaced
|
|
||||||
case .round:
|
|
||||||
self = .round
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Alignment: Codable {
|
enum Alignment: Codable {
|
||||||
@ -101,17 +89,6 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||||||
case center
|
case center
|
||||||
case right
|
case right
|
||||||
|
|
||||||
init(font: DrawingTextEntity.Alignment) {
|
|
||||||
switch font {
|
|
||||||
case .left:
|
|
||||||
self = .left
|
|
||||||
case .center:
|
|
||||||
self = .center
|
|
||||||
case .right:
|
|
||||||
self = .right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var alignment: NSTextAlignment {
|
var alignment: NSTextAlignment {
|
||||||
switch self {
|
switch self {
|
||||||
case .left:
|
case .left:
|
||||||
@ -567,7 +544,11 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||||||
|
|
||||||
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
|
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
|
||||||
|
|
||||||
let font: UIFont
|
if let (font, name) = availableFonts[text.string.lowercased()] {
|
||||||
|
self.textEntity.font = .custom(font, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var font: UIFont
|
||||||
switch self.textEntity.font {
|
switch self.textEntity.font {
|
||||||
case .sanFrancisco:
|
case .sanFrancisco:
|
||||||
font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||||
@ -577,7 +558,10 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||||||
font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
||||||
case .round:
|
case .round:
|
||||||
font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||||
|
case let .custom(fontName, _):
|
||||||
|
font = UIFont(name: fontName, size: fontSize) ?? Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||||
}
|
}
|
||||||
|
|
||||||
text.addAttribute(.font, value: font, range: range)
|
text.addAttribute(.font, value: font, range: range)
|
||||||
self.textView.font = font
|
self.textView.font = font
|
||||||
|
|
||||||
@ -761,6 +745,18 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
panGestureRecognizer.delegate = self
|
panGestureRecognizer.delegate = self
|
||||||
self.addGestureRecognizer(panGestureRecognizer)
|
self.addGestureRecognizer(panGestureRecognizer)
|
||||||
self.panGestureRecognizer = 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.onSnapToXAxis(snapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -784,16 +780,19 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let snapTool = DrawingEntitySnapTool()
|
||||||
|
|
||||||
private var currentHandle: CALayer?
|
private var currentHandle: CALayer?
|
||||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
|
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
|
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||||
|
|
||||||
if let sublayers = self.layer.sublayers {
|
if let sublayers = self.layer.sublayers {
|
||||||
for layer in sublayers {
|
for layer in sublayers {
|
||||||
if layer.frame.contains(location) {
|
if layer.frame.contains(location) {
|
||||||
@ -806,6 +805,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
case .changed:
|
case .changed:
|
||||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||||
|
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||||
|
|
||||||
var updatedScale = entity.scale
|
var updatedScale = entity.scale
|
||||||
var updatedPosition = entity.position
|
var updatedPosition = entity.position
|
||||||
@ -829,6 +829,8 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
} else if self.currentHandle === self.layer {
|
} else if self.currentHandle === self.layer {
|
||||||
updatedPosition.x += delta.x
|
updatedPosition.x += delta.x
|
||||||
updatedPosition.y += delta.y
|
updatedPosition.y += delta.y
|
||||||
|
|
||||||
|
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.scale = updatedScale
|
entity.scale = updatedScale
|
||||||
@ -838,7 +840,9 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
|
|
||||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||||
case .ended:
|
case .ended:
|
||||||
break
|
self.snapTool.reset()
|
||||||
|
case .cancelled:
|
||||||
|
self.snapTool.reset()
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -904,9 +908,18 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||||
|
|
||||||
self.border.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
let width: CGFloat = self.bounds.width - inset * 2.0
|
||||||
|
let height: CGFloat = self.bounds.height - inset * 2.0
|
||||||
|
let cornerRadius: CGFloat = 12.0 - self.scale
|
||||||
|
|
||||||
|
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
|
||||||
|
let count = 12
|
||||||
|
let relativeDashLength: CGFloat = 0.25
|
||||||
|
let dashLength = perimeter / CGFloat(count)
|
||||||
|
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
|
||||||
|
|
||||||
self.border.lineWidth = 2.0 / self.scale
|
self.border.lineWidth = 2.0 / self.scale
|
||||||
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: self.bounds.width - inset * 2.0, height: self.bounds.height - inset * 2.0)), cornerRadius: 12.0 / self.scale).cgPath
|
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1234,3 +1247,31 @@ class DrawingTextView: UITextView {
|
|||||||
self.typingAttributes = attributes
|
self.typingAttributes = attributes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var availableFonts: [String: (String, String)] = {
|
||||||
|
let familyNames = UIFont.familyNames
|
||||||
|
var result: [String: (String, String)] = [:]
|
||||||
|
|
||||||
|
for family in familyNames {
|
||||||
|
let names = UIFont.fontNames(forFamilyName: family)
|
||||||
|
|
||||||
|
var preferredFont: String?
|
||||||
|
for name in names {
|
||||||
|
let originalName = name
|
||||||
|
let name = name.lowercased()
|
||||||
|
if (!name.contains("-") || name.contains("regular")) && preferredFont == nil {
|
||||||
|
preferredFont = originalName
|
||||||
|
}
|
||||||
|
if name.contains("bold") && !name.contains("italic") {
|
||||||
|
preferredFont = originalName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let preferredFont {
|
||||||
|
let shortname = family.lowercased().replacingOccurrences(of: " ", with: "", options: [])
|
||||||
|
result[shortname] = (preferredFont, family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(result)
|
||||||
|
return result
|
||||||
|
}()
|
||||||
|
@ -310,104 +310,6 @@ final class NeonTool: DrawingElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PencilTool: DrawingElement {
|
|
||||||
let uuid = UUID()
|
|
||||||
|
|
||||||
let drawingSize: CGSize
|
|
||||||
let color: DrawingColor
|
|
||||||
let lineWidth: CGFloat
|
|
||||||
let arrow: Bool
|
|
||||||
|
|
||||||
var translation = CGPoint()
|
|
||||||
|
|
||||||
let renderLineWidth: CGFloat
|
|
||||||
var renderPath = UIBezierPath()
|
|
||||||
var renderAngle: CGFloat = 0.0
|
|
||||||
|
|
||||||
var bounds: CGRect {
|
|
||||||
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _points: [Polyline.Point] = []
|
|
||||||
var points: [Polyline.Point] {
|
|
||||||
return self._points.map { $0.offsetBy(self.translation) }
|
|
||||||
}
|
|
||||||
|
|
||||||
weak var metalView: DrawingMetalView?
|
|
||||||
|
|
||||||
func containsPoint(_ point: CGPoint) -> Bool {
|
|
||||||
return self.renderPath.contains(point.offsetBy(dx: -self.translation.x, dy: -self.translation.y))
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
|
||||||
let pathBoundingBox = path.bounds
|
|
||||||
if self.bounds.intersects(pathBoundingBox) {
|
|
||||||
for point in self._points {
|
|
||||||
if path.contains(point.location.offsetBy(self.translation)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
|
||||||
self.drawingSize = drawingSize
|
|
||||||
self.color = color
|
|
||||||
self.lineWidth = lineWidth
|
|
||||||
self.arrow = arrow
|
|
||||||
|
|
||||||
let minLineWidth = max(10.0, max(drawingSize.width, drawingSize.height) * 0.01)
|
|
||||||
let maxLineWidth = max(20.0, max(drawingSize.width, drawingSize.height) * 0.09)
|
|
||||||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
|
||||||
|
|
||||||
self.renderLineWidth = lineWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private var hot = false
|
|
||||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
|
||||||
guard case let .location(point) = path else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._points.isEmpty {
|
|
||||||
self.renderPath.move(to: point.location)
|
|
||||||
} else {
|
|
||||||
self.renderPath.addLine(to: point.location)
|
|
||||||
}
|
|
||||||
self._points.append(point)
|
|
||||||
|
|
||||||
self.hot = true
|
|
||||||
self.metalView?.updated(point, state: state, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
func draw(in context: CGContext, size: CGSize) {
|
|
||||||
guard !self._points.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
context.saveGState()
|
|
||||||
|
|
||||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
|
||||||
|
|
||||||
let hot = self.hot
|
|
||||||
if hot {
|
|
||||||
self.hot = false
|
|
||||||
} else {
|
|
||||||
self.metalView?.setup(self._points.map { $0.location }, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
|
||||||
}
|
|
||||||
self.metalView?.drawInContext(context)
|
|
||||||
if !hot {
|
|
||||||
self.metalView?.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
context.restoreGState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class FillTool: DrawingElement {
|
final class FillTool: DrawingElement {
|
||||||
let uuid = UUID()
|
let uuid = UUID()
|
||||||
|
|
||||||
@ -784,3 +686,106 @@ final class EraserTool: DrawingElement {
|
|||||||
renderLayer?.render(in: context)
|
renderLayer?.render(in: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//enum CodableDrawingElement {
|
||||||
|
// case pen(PenTool)
|
||||||
|
// case marker(MarkerTool)
|
||||||
|
// case neon(NeonTool)
|
||||||
|
// case eraser(EraserTool)
|
||||||
|
// case blur(BlurTool)
|
||||||
|
// case fill(FillTool)
|
||||||
|
//
|
||||||
|
// init?(element: DrawingElement) {
|
||||||
|
// if let element = element as? PenTool {
|
||||||
|
// self = .pen(element)
|
||||||
|
// } else if let element = element as? MarkerTool {
|
||||||
|
// self = .marker(element)
|
||||||
|
// } else if let element = element as? NeonTool {
|
||||||
|
// self = .neon(element)
|
||||||
|
// } else if let element = element as? EraserTool {
|
||||||
|
// self = .eraser(element)
|
||||||
|
// } else if let element = element as? BlurTool {
|
||||||
|
// self = .blur(element)
|
||||||
|
// } else if let element = element as? FillTool {
|
||||||
|
// self = .fill(element)
|
||||||
|
// } else {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var entity: DrawingElement {
|
||||||
|
// switch self {
|
||||||
|
// case let .pen(element):
|
||||||
|
// return element
|
||||||
|
// case let .marker(element):
|
||||||
|
// return element
|
||||||
|
// case let .neon(element):
|
||||||
|
// return element
|
||||||
|
// case let .eraser(element):
|
||||||
|
// return element
|
||||||
|
// case let .blur(element):
|
||||||
|
// return element
|
||||||
|
// case let .fill(element):
|
||||||
|
// return element
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//extension CodableDrawingElement: Codable {
|
||||||
|
// private enum CodingKeys: String, CodingKey {
|
||||||
|
// case type
|
||||||
|
// case element
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private enum ElementType: Int, Codable {
|
||||||
|
// case pen
|
||||||
|
// case marker
|
||||||
|
// case neon
|
||||||
|
// case eraser
|
||||||
|
// case blur
|
||||||
|
// case fill
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// init(from decoder: Decoder) throws {
|
||||||
|
// let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
// let type = try container.decode(ElementType.self, forKey: .type)
|
||||||
|
// switch type {
|
||||||
|
// case .pen:
|
||||||
|
// self = .pen(try container.decode(PenTool.self, forKey: .element))
|
||||||
|
// case .marker:
|
||||||
|
// self = .marker(try container.decode(MarkerTool.self, forKey: .element))
|
||||||
|
// case .neon:
|
||||||
|
// self = .neon(try container.decode(NeonTool.self, forKey: .element))
|
||||||
|
// case .eraser:
|
||||||
|
// self = .eraser(try container.decode(EraserTool.self, forKey: .element))
|
||||||
|
// case .blur:
|
||||||
|
// self = .blur(try container.decode(BlurTool.self, forKey: .element))
|
||||||
|
// case .fill:
|
||||||
|
// self = .fill(try container.decode(FillTool.self, forKey: .element))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func encode(to encoder: Encoder) throws {
|
||||||
|
// var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
// switch self {
|
||||||
|
// case let .pen(payload):
|
||||||
|
// try container.encode(ElementType.pen, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// case let .marker(payload):
|
||||||
|
// try container.encode(ElementType.marker, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// case let .neon(payload):
|
||||||
|
// try container.encode(ElementType.neon, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// case let .eraser(payload):
|
||||||
|
// try container.encode(ElementType.eraser, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// case let .blur(payload):
|
||||||
|
// try container.encode(ElementType.blur, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// case let .fill(payload):
|
||||||
|
// try container.encode(ElementType.fill, forKey: .type)
|
||||||
|
// try container.encode(payload, forKey: .element)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
@ -27,18 +27,10 @@ protocol DrawingElement: AnyObject {
|
|||||||
func draw(in: CGContext, size: CGSize)
|
func draw(in: CGContext, size: CGSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DrawingCommand {
|
enum DrawingOperation {
|
||||||
enum DrawingElementTransform {
|
case element(DrawingElement)
|
||||||
case move(offset: CGPoint)
|
case addEntity(UUID)
|
||||||
}
|
|
||||||
|
|
||||||
case addStroke(DrawingElement)
|
|
||||||
case updateStrokes([UUID], DrawingElementTransform)
|
|
||||||
case removeStroke(DrawingElement)
|
|
||||||
case addEntity(DrawingEntity)
|
|
||||||
case updateEntity(UUID, DrawingEntity)
|
|
||||||
case removeEntity(DrawingEntity)
|
case removeEntity(DrawingEntity)
|
||||||
case updateEntityZOrder(UUID, Int32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView {
|
public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView {
|
||||||
@ -63,10 +55,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
case pen
|
case pen
|
||||||
case marker
|
case marker
|
||||||
case neon
|
case neon
|
||||||
case pencil
|
|
||||||
case eraser
|
case eraser
|
||||||
case lasso
|
case lasso
|
||||||
case objectRemover
|
|
||||||
case blur
|
case blur
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +72,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
var getFullImage: (Bool) -> UIImage? = { _ in return nil }
|
var getFullImage: (Bool) -> UIImage? = { _ in return nil }
|
||||||
|
|
||||||
private var elements: [DrawingElement] = []
|
private var elements: [DrawingElement] = []
|
||||||
private var redoElements: [DrawingElement] = []
|
private var undoStack: [DrawingOperation] = []
|
||||||
|
private var redoStack: [DrawingOperation] = []
|
||||||
fileprivate var uncommitedElement: DrawingElement?
|
fileprivate var uncommitedElement: DrawingElement?
|
||||||
|
|
||||||
private(set) var drawingImage: UIImage?
|
private(set) var drawingImage: UIImage?
|
||||||
@ -109,6 +100,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
private var strokeRecognitionTimer: SwiftSignalKit.Timer?
|
private var strokeRecognitionTimer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
private var isDrawing = false
|
private var isDrawing = false
|
||||||
|
private var drawingGestureStartTimestamp: Double?
|
||||||
|
|
||||||
private func loadTemplates() {
|
private func loadTemplates() {
|
||||||
func load(_ name: String) {
|
func load(_ name: String) {
|
||||||
@ -134,7 +126,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
load("shape_arrow")
|
load("shape_arrow")
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(size: CGSize) {
|
init(size: CGSize) {
|
||||||
self.imageSize = size
|
self.imageSize = size
|
||||||
|
|
||||||
let format = UIGraphicsImageRendererFormat()
|
let format = UIGraphicsImageRendererFormat()
|
||||||
@ -203,20 +195,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if case .objectRemover = strongSelf.tool {
|
if case .lasso = strongSelf.tool {
|
||||||
if case let .location(point) = path {
|
|
||||||
var elementsToRemove: [DrawingElement] = []
|
|
||||||
for element in strongSelf.elements {
|
|
||||||
if element.containsPoint(point.location) {
|
|
||||||
elementsToRemove.append(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for element in elementsToRemove {
|
|
||||||
strongSelf.removeElement(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if case .lasso = strongSelf.tool {
|
|
||||||
if case let .smoothCurve(bezierPath) = path {
|
if case let .smoothCurve(bezierPath) = path {
|
||||||
let scale = strongSelf.bounds.width / strongSelf.imageSize.width
|
let scale = strongSelf.bounds.width / strongSelf.imageSize.width
|
||||||
|
|
||||||
@ -254,6 +233,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
case .began:
|
case .began:
|
||||||
strongSelf.isDrawing = true
|
strongSelf.isDrawing = true
|
||||||
strongSelf.previousStrokePoint = nil
|
strongSelf.previousStrokePoint = nil
|
||||||
|
strongSelf.drawingGestureStartTimestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
if strongSelf.uncommitedElement != nil {
|
if strongSelf.uncommitedElement != nil {
|
||||||
strongSelf.finishDrawing()
|
strongSelf.finishDrawing()
|
||||||
@ -263,7 +243,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if newElement is MarkerTool || newElement is PencilTool {
|
if newElement is MarkerTool {
|
||||||
self?.metalView.isHidden = false
|
self?.metalView.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,6 +263,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
|
|
||||||
if case let .polyline(line) = path, let lastPoint = line.points.last {
|
if case let .polyline(line) = path, let lastPoint = line.points.last {
|
||||||
if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 {
|
if let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 {
|
||||||
|
let currentTimestamp = CACurrentMediaTime()
|
||||||
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
||||||
strongSelf.previousStrokePoint = lastPoint.location
|
strongSelf.previousStrokePoint = lastPoint.location
|
||||||
|
|
||||||
@ -290,7 +271,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
strongSelf.strokeRecognitionTimer = nil
|
strongSelf.strokeRecognitionTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.strokeRecognitionTimer == nil {
|
if strongSelf.strokeRecognitionTimer == nil, let startTimestamp = strongSelf.drawingGestureStartTimestamp, currentTimestamp - startTimestamp < 3.0 {
|
||||||
strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in
|
strongSelf.strokeRecognitionTimer = SwiftSignalKit.Timer(timeout: 0.85, repeat: false, completion: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -298,31 +279,30 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 {
|
if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 {
|
||||||
let strokeRecognizer = Unistroke(points: line.points.map { $0.location })
|
let strokeRecognizer = Unistroke(points: line.points.map { $0.location })
|
||||||
if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) {
|
if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) {
|
||||||
|
|
||||||
let edges = line.bounds
|
let edges = line.bounds
|
||||||
let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY))
|
let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY))
|
||||||
|
|
||||||
var entity: DrawingEntity?
|
var entity: DrawingEntity?
|
||||||
if template == "shape_rectangle" {
|
if template == "shape_rectangle" {
|
||||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .rectangle, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||||
shapeEntity.position = bounds.center
|
shapeEntity.position = bounds.center
|
||||||
shapeEntity.size = bounds.size
|
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||||
entity = shapeEntity
|
entity = shapeEntity
|
||||||
} else if template == "shape_circle" {
|
} else if template == "shape_circle" {
|
||||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .ellipse, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||||
shapeEntity.position = bounds.center
|
shapeEntity.position = bounds.center
|
||||||
shapeEntity.size = bounds.size
|
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||||
entity = shapeEntity
|
entity = shapeEntity
|
||||||
} else if template == "shape_star" {
|
} else if template == "shape_star" {
|
||||||
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: 0.25)
|
let shapeEntity = DrawingSimpleShapeEntity(shapeType: .star, drawType: .stroke, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||||
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
shapeEntity.referenceDrawingSize = strongSelf.imageSize
|
||||||
shapeEntity.position = bounds.center
|
shapeEntity.position = bounds.center
|
||||||
shapeEntity.size = CGSize(width: max(bounds.width, bounds.height), height: max(bounds.width, bounds.height))
|
shapeEntity.size = CGSize(width: max(bounds.width, bounds.height) * 1.1, height: max(bounds.width, bounds.height) * 1.1)
|
||||||
entity = shapeEntity
|
entity = shapeEntity
|
||||||
} else if template == "shape_arrow" {
|
} else if template == "shape_arrow" {
|
||||||
let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: 0.2)
|
let arrowEntity = DrawingVectorEntity(type: .oneSidedArrow, color: strongSelf.toolColor, lineWidth: strongSelf.toolBrushSize)
|
||||||
arrowEntity.referenceDrawingSize = strongSelf.imageSize
|
arrowEntity.referenceDrawingSize = strongSelf.imageSize
|
||||||
arrowEntity.start = line.points.first?.location ?? .zero
|
arrowEntity.start = line.points.first?.location ?? .zero
|
||||||
arrowEntity.end = line.points[line.points.count - 4].location
|
arrowEntity.end = line.points[line.points.count - 4].location
|
||||||
@ -331,6 +311,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
|
|
||||||
if let entity = entity {
|
if let entity = entity {
|
||||||
strongSelf.entitiesView?.add(entity)
|
strongSelf.entitiesView?.add(entity)
|
||||||
|
strongSelf.entitiesView?.selectEntity(entity)
|
||||||
strongSelf.cancelDrawing()
|
strongSelf.cancelDrawing()
|
||||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false
|
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false
|
||||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true
|
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true
|
||||||
@ -561,9 +542,10 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
let complete: (Bool) -> Void = { synchronous in
|
let complete: (Bool) -> Void = { synchronous in
|
||||||
self.commit(interactive: true, synchronous: synchronous)
|
self.commit(interactive: true, synchronous: synchronous)
|
||||||
|
|
||||||
self.redoElements.removeAll()
|
self.redoStack.removeAll()
|
||||||
if let uncommitedElement = self.uncommitedElement {
|
if let uncommitedElement = self.uncommitedElement {
|
||||||
self.elements.append(uncommitedElement)
|
self.elements.append(uncommitedElement)
|
||||||
|
self.undoStack.append(.element(uncommitedElement))
|
||||||
self.uncommitedElement = nil
|
self.uncommitedElement = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -584,7 +566,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
|
|
||||||
self.uncommitedElement = nil
|
self.uncommitedElement = nil
|
||||||
self.elements.removeAll()
|
self.elements.removeAll()
|
||||||
self.redoElements.removeAll()
|
self.undoStack.removeAll()
|
||||||
|
self.redoStack.removeAll()
|
||||||
|
|
||||||
let snapshotView = UIImageView(image: self.drawingImage)
|
let snapshotView = UIImageView(image: self.drawingImage)
|
||||||
snapshotView.frame = self.bounds
|
snapshotView.frame = self.bounds
|
||||||
@ -605,38 +588,89 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func undo() {
|
private func undo() {
|
||||||
guard let lastElement = self.elements.last else {
|
guard let lastOperation = self.undoStack.last else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.uncommitedElement = nil
|
switch lastOperation {
|
||||||
self.redoElements.append(lastElement)
|
case let .element(element):
|
||||||
self.elements.removeLast()
|
self.uncommitedElement = nil
|
||||||
|
self.redoStack.append(.element(element))
|
||||||
|
self.elements.removeAll(where: { $0.uuid == element.uuid })
|
||||||
|
|
||||||
let snapshotView = UIImageView(image: self.drawingImage)
|
let snapshotView = UIImageView(image: self.drawingImage)
|
||||||
snapshotView.frame = self.bounds
|
snapshotView.frame = self.bounds
|
||||||
self.addSubview(snapshotView)
|
self.addSubview(snapshotView)
|
||||||
self.commit(reset: true)
|
self.commit(reset: true)
|
||||||
|
|
||||||
Queue.mainQueue().justDispatch {
|
Queue.mainQueue().justDispatch {
|
||||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||||
snapshotView?.removeFromSuperview()
|
snapshotView?.removeFromSuperview()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
case let .addEntity(uuid):
|
||||||
|
if let entityView = self.entitiesView?.getView(for: uuid) {
|
||||||
|
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
|
||||||
|
self.redoStack.append(.removeEntity(entityView.entity))
|
||||||
|
}
|
||||||
|
case let .removeEntity(entity):
|
||||||
|
if let view = self.entitiesView?.add(entity, announce: false) {
|
||||||
|
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
if !(entity is DrawingVectorEntity) {
|
||||||
|
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.redoStack.append(.addEntity(entity.uuid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.undoStack.removeLast()
|
||||||
|
|
||||||
self.updateInternalState()
|
self.updateInternalState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func redo() {
|
private func redo() {
|
||||||
guard let lastElement = self.redoElements.last else {
|
guard let lastOperation = self.redoStack.last else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.uncommitedElement = nil
|
|
||||||
self.elements.append(lastElement)
|
|
||||||
self.redoElements.removeLast()
|
|
||||||
self.uncommitedElement = lastElement
|
|
||||||
|
|
||||||
self.commit(reset: false)
|
switch lastOperation {
|
||||||
self.uncommitedElement = nil
|
case let .element(element):
|
||||||
|
self.uncommitedElement = nil
|
||||||
|
self.elements.append(element)
|
||||||
|
self.undoStack.append(.element(element))
|
||||||
|
self.uncommitedElement = element
|
||||||
|
|
||||||
|
self.commit(reset: false)
|
||||||
|
self.uncommitedElement = nil
|
||||||
|
case let .addEntity(uuid):
|
||||||
|
if let entityView = self.entitiesView?.getView(for: uuid) {
|
||||||
|
self.entitiesView?.remove(uuid: uuid, animated: true, announce: false)
|
||||||
|
self.undoStack.append(.removeEntity(entityView.entity))
|
||||||
|
}
|
||||||
|
case let .removeEntity(entity):
|
||||||
|
if let view = self.entitiesView?.add(entity, announce: false) {
|
||||||
|
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
if !(entity is DrawingVectorEntity) {
|
||||||
|
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.undoStack.append(.addEntity(entity.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redoStack.removeLast()
|
||||||
|
|
||||||
|
self.updateInternalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onEntityAdded(_ entity: DrawingEntity) {
|
||||||
|
self.redoStack.removeAll()
|
||||||
|
self.undoStack.append(.addEntity(entity.uuid))
|
||||||
|
|
||||||
|
self.updateInternalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func onEntityRemoved(_ entity: DrawingEntity) {
|
||||||
|
self.redoStack.removeAll()
|
||||||
|
self.undoStack.append(.removeEntity(entity))
|
||||||
|
|
||||||
self.updateInternalState()
|
self.updateInternalState()
|
||||||
}
|
}
|
||||||
@ -723,9 +757,9 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
|
|
||||||
private func updateInternalState() {
|
private func updateInternalState() {
|
||||||
self.stateUpdated(NavigationState(
|
self.stateUpdated(NavigationState(
|
||||||
canUndo: !self.elements.isEmpty,
|
canUndo: !self.elements.isEmpty || !self.undoStack.isEmpty,
|
||||||
canRedo: !self.redoElements.isEmpty,
|
canRedo: !self.redoStack.isEmpty,
|
||||||
canClear: !self.elements.isEmpty,
|
canClear: !self.elements.isEmpty || !(self.entitiesView?.entities.isEmpty ?? true),
|
||||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
||||||
isDrawing: self.isDrawing
|
isDrawing: self.isDrawing
|
||||||
))
|
))
|
||||||
@ -764,15 +798,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||||||
lineWidth: self.toolBrushSize * scale,
|
lineWidth: self.toolBrushSize * scale,
|
||||||
arrow: self.toolHasArrow
|
arrow: self.toolHasArrow
|
||||||
)
|
)
|
||||||
case .pencil:
|
|
||||||
let pencilTool = PencilTool(
|
|
||||||
drawingSize: self.imageSize,
|
|
||||||
color: self.toolColor,
|
|
||||||
lineWidth: self.toolBrushSize * scale,
|
|
||||||
arrow: self.toolHasArrow
|
|
||||||
)
|
|
||||||
pencilTool.metalView = self.metalView
|
|
||||||
element = pencilTool
|
|
||||||
case .blur:
|
case .blur:
|
||||||
let blurTool = BlurTool(
|
let blurTool = BlurTool(
|
||||||
drawingSize: self.imageSize,
|
drawingSize: self.imageSize,
|
||||||
|
@ -4,20 +4,46 @@ import Display
|
|||||||
|
|
||||||
final class PenTool: DrawingElement {
|
final class PenTool: DrawingElement {
|
||||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||||
|
var segmentsCount = 0
|
||||||
|
|
||||||
|
var displayLink: ConstantDisplayLinkAnimator?
|
||||||
func setup(size: CGSize) {
|
func setup(size: CGSize) {
|
||||||
self.shouldRasterize = true
|
self.shouldRasterize = true
|
||||||
self.contentsScale = 1.0
|
self.contentsScale = 1.0
|
||||||
|
|
||||||
let bounds = CGRect(origin: .zero, size: size)
|
let bounds = CGRect(origin: .zero, size: size)
|
||||||
self.frame = bounds
|
self.frame = bounds
|
||||||
|
|
||||||
|
self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if let line = strongSelf.line, strongSelf.segmentsCount < line.count, let velocity = strongSelf.velocity {
|
||||||
|
let delta = max(9, Int(velocity / 100.0))
|
||||||
|
let start = strongSelf.segmentsCount
|
||||||
|
strongSelf.segmentsCount = min(strongSelf.segmentsCount + delta, line.count)
|
||||||
|
|
||||||
|
let rect = line.rect(from: start, to: strongSelf.segmentsCount)
|
||||||
|
strongSelf.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.displayLink?.frameInterval = 1
|
||||||
|
self.displayLink?.isPaused = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var color: UIColor?
|
private var color: UIColor?
|
||||||
private var line: StrokeLine?
|
private var line: StrokeLine?
|
||||||
fileprivate func draw(line: StrokeLine, color: UIColor, rect: CGRect) {
|
private var velocity: CGFloat?
|
||||||
|
private var previousRect: CGRect?
|
||||||
|
fileprivate func draw(line: StrokeLine, velocity: CGFloat, color: UIColor, rect: CGRect) {
|
||||||
self.line = line
|
self.line = line
|
||||||
self.color = color
|
self.color = color
|
||||||
|
self.previousRect = rect
|
||||||
|
if let previous = self.velocity {
|
||||||
|
self.velocity = velocity * 0.4 + previous * 0.6
|
||||||
|
} else {
|
||||||
|
self.velocity = velocity
|
||||||
|
}
|
||||||
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +74,7 @@ final class PenTool: DrawingElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func draw(in ctx: CGContext) {
|
override func draw(in ctx: CGContext) {
|
||||||
self.line?.drawInContext(ctx)
|
self.line?.drawInContext(ctx, upTo: self.segmentsCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +192,7 @@ final class PenTool: DrawingElement {
|
|||||||
|
|
||||||
let rect = self.renderLine.draw(at: point)
|
let rect = self.renderLine.draw(at: point)
|
||||||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||||
currentRenderLayer.draw(line: self.renderLine, color: self.color.toUIColor(), rect: rect)
|
currentRenderLayer.draw(line: self.renderLine, velocity: point.velocity, color: self.color.toUIColor(), rect: rect)
|
||||||
}
|
}
|
||||||
|
|
||||||
if state == .ended {
|
if state == .ended {
|
||||||
@ -239,6 +265,7 @@ private class StrokeLine {
|
|||||||
let d: CGPoint
|
let d: CGPoint
|
||||||
let abWidth: CGFloat
|
let abWidth: CGFloat
|
||||||
let cdWidth: CGFloat
|
let cdWidth: CGFloat
|
||||||
|
let rect: CGRect
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Point {
|
struct Point {
|
||||||
@ -281,8 +308,8 @@ private class StrokeLine {
|
|||||||
return appendPoint(point)
|
return appendPoint(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawInContext(_ context: CGContext) {
|
func drawInContext(_ context: CGContext, upTo: Int? = nil) {
|
||||||
self.drawSegments(self.segments, inContext: context)
|
self.drawSegments(self.segments, upTo: upTo ?? self.segments.count, inContext: context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractLineWidth(from velocity: CGFloat) -> CGFloat {
|
func extractLineWidth(from velocity: CGFloat) -> CGFloat {
|
||||||
@ -395,18 +422,48 @@ private class StrokeLine {
|
|||||||
let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
||||||
let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
||||||
|
|
||||||
updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY))
|
let segmentRect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||||
|
updateRect = updateRect.union(segmentRect)
|
||||||
|
|
||||||
segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth))
|
segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth, rect: segmentRect))
|
||||||
}
|
}
|
||||||
return (segments, updateRect)
|
return (segments, updateRect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawSegments(_ segments: [Segment], inContext context: CGContext) {
|
var count: Int {
|
||||||
for segment in segments {
|
return self.segments.count
|
||||||
context.beginPath()
|
}
|
||||||
|
|
||||||
//let color = [UIColor.red, UIColor.green, UIColor.blue, UIColor.yellow].randomElement()!
|
func rect(from: Int, to: Int) -> CGRect {
|
||||||
|
var minX: CGFloat = .greatestFiniteMagnitude
|
||||||
|
var minY: CGFloat = .greatestFiniteMagnitude
|
||||||
|
var maxX: CGFloat = 0.0
|
||||||
|
var maxY: CGFloat = 0.0
|
||||||
|
|
||||||
|
for i in from ..< to {
|
||||||
|
let segment = self.segments[i]
|
||||||
|
|
||||||
|
if segment.rect.minX < minX {
|
||||||
|
minX = segment.rect.minX
|
||||||
|
}
|
||||||
|
if segment.rect.maxX > maxX {
|
||||||
|
maxX = segment.rect.maxX
|
||||||
|
}
|
||||||
|
if segment.rect.minY < minY {
|
||||||
|
minY = segment.rect.minY
|
||||||
|
}
|
||||||
|
if segment.rect.maxY > maxY {
|
||||||
|
maxY = segment.rect.maxY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawSegments(_ segments: [Segment], upTo: Int, inContext context: CGContext) {
|
||||||
|
for i in 0 ..< upTo {
|
||||||
|
let segment = segments[i]
|
||||||
|
context.beginPath()
|
||||||
|
|
||||||
context.setStrokeColor(color.cgColor)
|
context.setStrokeColor(color.cgColor)
|
||||||
context.setFillColor(color.cgColor)
|
context.setFillColor(color.cgColor)
|
||||||
|
@ -44,11 +44,12 @@ enum DrawingTextAlignment: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DrawingTextFont: Equatable, CaseIterable {
|
enum DrawingTextFont: Equatable, Hashable {
|
||||||
case sanFrancisco
|
case sanFrancisco
|
||||||
case newYork
|
case newYork
|
||||||
case monospaced
|
case monospaced
|
||||||
case round
|
case round
|
||||||
|
case custom(String, String)
|
||||||
|
|
||||||
init(font: DrawingTextEntity.Font) {
|
init(font: DrawingTextEntity.Font) {
|
||||||
switch font {
|
switch font {
|
||||||
@ -60,6 +61,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
|||||||
self = .monospaced
|
self = .monospaced
|
||||||
case .round:
|
case .round:
|
||||||
self = .round
|
self = .round
|
||||||
|
case let .custom(font, name):
|
||||||
|
self = .custom(font, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +76,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
|||||||
return .monospaced
|
return .monospaced
|
||||||
case .round:
|
case .round:
|
||||||
return .round
|
return .round
|
||||||
|
case let .custom(font, name):
|
||||||
|
return .custom(font, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +91,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
|||||||
return "Monospaced"
|
return "Monospaced"
|
||||||
case .round:
|
case .round:
|
||||||
return "Rounded"
|
return "Rounded"
|
||||||
|
case let .custom(_, name):
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +106,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
|||||||
return Font.with(size: 13.0, design: .monospace, weight: .semibold)
|
return Font.with(size: 13.0, design: .monospace, weight: .semibold)
|
||||||
case .round:
|
case .round:
|
||||||
return Font.with(size: 13.0, design: .round, weight: .semibold)
|
return Font.with(size: 13.0, design: .round, weight: .semibold)
|
||||||
|
case let .custom(font, _):
|
||||||
|
return UIFont(name: font, size: 13.0) ?? Font.semibold(13.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -355,7 +364,10 @@ final class TextFontComponent: Component {
|
|||||||
|
|
||||||
contentWidth += 36.0
|
contentWidth += 36.0
|
||||||
|
|
||||||
|
var validIds = Set<DrawingTextFont>()
|
||||||
for value in component.values {
|
for value in component.values {
|
||||||
|
validIds.insert(value)
|
||||||
|
|
||||||
contentWidth += 12.0
|
contentWidth += 12.0
|
||||||
let button: HighlightableButton
|
let button: HighlightableButton
|
||||||
if let current = self.buttons[value] {
|
if let current = self.buttons[value] {
|
||||||
@ -387,6 +399,13 @@ final class TextFontComponent: Component {
|
|||||||
}
|
}
|
||||||
contentWidth += 12.0
|
contentWidth += 12.0
|
||||||
|
|
||||||
|
for (font, button) in self.buttons {
|
||||||
|
if !validIds.contains(font) {
|
||||||
|
button.removeFromSuperview()
|
||||||
|
self.buttons[font] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.scrollView.contentSize.width != contentWidth {
|
if self.scrollView.contentSize.width != contentWidth {
|
||||||
self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0)
|
self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0)
|
||||||
}
|
}
|
||||||
@ -591,6 +610,16 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
fontAvailableWidth -= 72.0
|
fontAvailableWidth -= 72.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fonts: [DrawingTextFont] = [
|
||||||
|
.sanFrancisco,
|
||||||
|
.newYork,
|
||||||
|
.monospaced,
|
||||||
|
.round
|
||||||
|
]
|
||||||
|
if case .custom = component.font {
|
||||||
|
fonts.insert(component.font, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
let font = font.update(
|
let font = font.update(
|
||||||
component: TextFontComponent(
|
component: TextFontComponent(
|
||||||
styleButton: AnyComponent(
|
styleButton: AnyComponent(
|
||||||
@ -617,7 +646,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||||
),
|
),
|
||||||
values: DrawingTextFont.allCases,
|
values: fonts,
|
||||||
selectedValue: component.font,
|
selectedValue: component.font,
|
||||||
tag: component.tag,
|
tag: component.tag,
|
||||||
updated: { font in
|
updated: { font in
|
||||||
|
@ -1013,42 +1013,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
|
|
||||||
func setupItem(_ item: UniversalVideoGalleryItem) {
|
func setupItem(_ item: UniversalVideoGalleryItem) {
|
||||||
if self.item?.content.id != item.content.id {
|
if self.item?.content.id != item.content.id {
|
||||||
func parseChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] {
|
var chapters = parseMediaPlayerChapters(item.caption)
|
||||||
var existingTimecodes = Set<Double>()
|
|
||||||
var timecodeRanges: [(NSRange, TelegramTimecode)] = []
|
|
||||||
var lineRanges: [NSRange] = []
|
|
||||||
string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in
|
|
||||||
if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
|
||||||
if !existingTimecodes.contains(timecode.time) {
|
|
||||||
timecodeRanges.append((range, timecode))
|
|
||||||
existingTimecodes.insert(timecode.time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
(string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in
|
|
||||||
lineRanges.append(range)
|
|
||||||
})
|
|
||||||
|
|
||||||
var chapters: [MediaPlayerScrubbingChapter] = []
|
|
||||||
for (timecodeRange, timecode) in timecodeRanges {
|
|
||||||
inner: for lineRange in lineRanges {
|
|
||||||
if lineRange.contains(timecodeRange.location) {
|
|
||||||
if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 {
|
|
||||||
var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "")
|
|
||||||
title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
|
|
||||||
chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time))
|
|
||||||
}
|
|
||||||
break inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
var chapters = parseChapters(item.caption)
|
|
||||||
if chapters.isEmpty, let description = item.description {
|
if chapters.isEmpty, let description = item.description {
|
||||||
chapters = parseChapters(description)
|
chapters = parseMediaPlayerChapters(description)
|
||||||
}
|
}
|
||||||
let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters)
|
let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters)
|
||||||
self.scrubberView = scrubberView
|
self.scrubberView = scrubberView
|
||||||
@ -2710,7 +2677,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||||||
}
|
}
|
||||||
let baseNavigationController = strongSelf.baseNavigationController()
|
let baseNavigationController = strongSelf.baseNavigationController()
|
||||||
baseNavigationController?.view.endEditing(true)
|
baseNavigationController?.view.endEditing(true)
|
||||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: Array(packs.prefix(1)), sendSticker: nil, actionPerformed: { actions in
|
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packs[0], stickerPacks: packs, sendSticker: nil, actionPerformed: { actions in
|
||||||
if let (info, items, action) = actions.first {
|
if let (info, items, action) = actions.first {
|
||||||
let animateInAsReplacement = false
|
let animateInAsReplacement = false
|
||||||
switch action {
|
switch action {
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
|
|
||||||
@property (nonatomic, readonly) bool hasAnimation;
|
@property (nonatomic, readonly) bool hasAnimation;
|
||||||
|
|
||||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation;
|
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers;
|
||||||
|
|
||||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation;
|
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers;
|
||||||
|
|
||||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath;
|
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath;
|
||||||
|
|
||||||
|
@ -75,6 +75,9 @@
|
|||||||
|
|
||||||
@protocol TGPhotoDrawingEntitiesView <NSObject>
|
@protocol TGPhotoDrawingEntitiesView <NSObject>
|
||||||
|
|
||||||
|
@property (nonatomic, copy) CGPoint (^getEntityCenterPosition)(void);
|
||||||
|
@property (nonatomic, copy) CGFloat (^getEntityInitialRotation)(void);
|
||||||
|
|
||||||
@property (nonatomic, copy) void(^hasSelectionChanged)(bool);
|
@property (nonatomic, copy) void(^hasSelectionChanged)(bool);
|
||||||
@property (nonatomic, readonly) BOOL hasSelection;
|
@property (nonatomic, readonly) BOOL hasSelection;
|
||||||
|
|
||||||
@ -107,6 +110,7 @@
|
|||||||
safeInsets:(UIEdgeInsets)safeInsets
|
safeInsets:(UIEdgeInsets)safeInsets
|
||||||
statusBarHeight:(CGFloat)statusBarHeight
|
statusBarHeight:(CGFloat)statusBarHeight
|
||||||
inputHeight:(CGFloat)inputHeight
|
inputHeight:(CGFloat)inputHeight
|
||||||
|
orientation:(UIInterfaceOrientation)orientation
|
||||||
animated:(BOOL)animated;
|
animated:(BOOL)animated;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -158,12 +158,24 @@
|
|||||||
if (strongController == nil)
|
if (strongController == nil)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (strongSelf.didFinishWithVideo != nil)
|
if (strongSelf.willFinishWithVideo != nil) {
|
||||||
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
strongSelf.willFinishWithVideo(image, ^{
|
||||||
|
if (strongSelf.didFinishWithVideo != nil)
|
||||||
|
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
||||||
|
|
||||||
commit();
|
commit();
|
||||||
|
|
||||||
[strongController dismissAnimated:false];
|
[strongController dismissAnimated:false];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (strongSelf.didFinishWithVideo != nil)
|
||||||
|
strongSelf.didFinishWithVideo(image,
|
||||||
|
asset, adjustments);
|
||||||
|
|
||||||
|
commit();
|
||||||
|
|
||||||
|
[strongController dismissAnimated:false];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
[itemViews addObject:carouselItem];
|
[itemViews addObject:carouselItem];
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
@implementation TGPaintingData
|
@implementation TGPaintingData
|
||||||
|
|
||||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation
|
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers
|
||||||
{
|
{
|
||||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||||
paintingData->_drawingData = data;
|
paintingData->_drawingData = data;
|
||||||
@ -28,14 +28,16 @@
|
|||||||
paintingData->_stillImage = stillImage;
|
paintingData->_stillImage = stillImage;
|
||||||
paintingData->_entitiesData = entitiesData;
|
paintingData->_entitiesData = entitiesData;
|
||||||
paintingData->_hasAnimation = hasAnimation;
|
paintingData->_hasAnimation = hasAnimation;
|
||||||
|
paintingData->_stickers = stickers;
|
||||||
return paintingData;
|
return paintingData;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation {
|
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData hasAnimation:(bool)hasAnimation stickers:(NSArray *)stickers {
|
||||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||||
paintingData->_imagePath = imagePath;
|
paintingData->_imagePath = imagePath;
|
||||||
paintingData->_entitiesData = entitiesData;
|
paintingData->_entitiesData = entitiesData;
|
||||||
paintingData->_hasAnimation = hasAnimation;
|
paintingData->_hasAnimation = hasAnimation;
|
||||||
|
paintingData->_stickers = stickers;
|
||||||
return paintingData;
|
return paintingData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +53,7 @@
|
|||||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||||
paintingData->_entitiesData = _entitiesData;
|
paintingData->_entitiesData = _entitiesData;
|
||||||
paintingData->_hasAnimation = _hasAnimation;
|
paintingData->_hasAnimation = _hasAnimation;
|
||||||
|
paintingData->_stickers = _stickers;
|
||||||
return paintingData;
|
return paintingData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,18 +125,6 @@
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray *)stickers
|
|
||||||
{
|
|
||||||
return @[];
|
|
||||||
// NSMutableSet *stickers = [[NSMutableSet alloc] init];
|
|
||||||
// for (TGPhotoPaintEntity *entity in self.entities)
|
|
||||||
// {
|
|
||||||
// if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
|
|
||||||
// [stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
|
|
||||||
// }
|
|
||||||
// return [stickers allObjects];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)isEqual:(id)object
|
- (BOOL)isEqual:(id)object
|
||||||
{
|
{
|
||||||
if (object == self)
|
if (object == self)
|
||||||
|
@ -219,6 +219,22 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
|||||||
strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection;
|
strongSelf->_scrollView.pinchGestureRecognizer.enabled = !hasSelection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_entitiesView.getEntityCenterPosition = ^CGPoint {
|
||||||
|
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||||
|
if (strongSelf == nil)
|
||||||
|
return CGPointZero;
|
||||||
|
|
||||||
|
return [strongSelf entityCenterPoint];
|
||||||
|
};
|
||||||
|
|
||||||
|
_entitiesView.getEntityInitialRotation = ^CGFloat {
|
||||||
|
__strong TGPhotoDrawingController *strongSelf = weakSelf;
|
||||||
|
if (strongSelf == nil)
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
|
return [strongSelf entityInitialRotation];
|
||||||
|
};
|
||||||
|
|
||||||
[self.view setNeedsLayout];
|
[self.view setNeedsLayout];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,6 +787,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
|||||||
safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right)
|
safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right)
|
||||||
statusBarHeight:[_context statusBarFrame].size.height
|
statusBarHeight:[_context statusBarFrame].size.height
|
||||||
inputHeight:_keyboardHeight
|
inputHeight:_keyboardHeight
|
||||||
|
orientation:self.effectiveOrientation
|
||||||
animated:animated];
|
animated:animated];
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -878,6 +895,16 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
|||||||
return UIRectEdgeTop | UIRectEdgeBottom;
|
return UIRectEdgeTop | UIRectEdgeBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (CGPoint)entityCenterPoint
|
||||||
|
{
|
||||||
|
return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGFloat)entityInitialRotation
|
||||||
|
{
|
||||||
|
return TGCounterRotationForOrientation(_photoEditor.cropOrientation) - _photoEditor.cropRotation;
|
||||||
|
}
|
||||||
|
|
||||||
+ (CGSize)maximumPaintingSize
|
+ (CGSize)maximumPaintingSize
|
||||||
{
|
{
|
||||||
static dispatch_once_t onceToken;
|
static dispatch_once_t onceToken;
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
#import <objc/runtime.h>
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
#import <LegacyComponents/ASWatcher.h>
|
|
||||||
|
|
||||||
#import <Photos/Photos.h>
|
#import <Photos/Photos.h>
|
||||||
|
|
||||||
#import <LegacyComponents/TGPhotoEditorAnimation.h>
|
#import <LegacyComponents/TGPhotoEditorAnimation.h>
|
||||||
@ -52,7 +50,7 @@
|
|||||||
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
|
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
|
||||||
#import "TGCameraCapturedVideo.h"
|
#import "TGCameraCapturedVideo.h"
|
||||||
|
|
||||||
@interface TGPhotoEditorController () <ASWatcher, TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
@interface TGPhotoEditorController () <TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||||
{
|
{
|
||||||
bool _switchingTab;
|
bool _switchingTab;
|
||||||
TGPhotoEditorTab _availableTabs;
|
TGPhotoEditorTab _availableTabs;
|
||||||
@ -102,7 +100,6 @@
|
|||||||
bool _hasOpenedPhotoTools;
|
bool _hasOpenedPhotoTools;
|
||||||
bool _hiddenToolbarView;
|
bool _hiddenToolbarView;
|
||||||
|
|
||||||
TGMenuContainerView *_menuContainerView;
|
|
||||||
UIDocumentInteractionController *_documentController;
|
UIDocumentInteractionController *_documentController;
|
||||||
|
|
||||||
bool _dismissed;
|
bool _dismissed;
|
||||||
@ -136,15 +133,12 @@
|
|||||||
|
|
||||||
@implementation TGPhotoEditorController
|
@implementation TGPhotoEditorController
|
||||||
|
|
||||||
@synthesize actionHandle = _actionHandle;
|
|
||||||
|
|
||||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab
|
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab
|
||||||
{
|
{
|
||||||
self = [super initWithContext:context];
|
self = [super initWithContext:context];
|
||||||
if (self != nil)
|
if (self != nil)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true];
|
|
||||||
|
|
||||||
self.automaticallyManageScrollViewInsets = false;
|
self.automaticallyManageScrollViewInsets = false;
|
||||||
self.autoManageStatusBarBackground = false;
|
self.autoManageStatusBarBackground = false;
|
||||||
@ -195,7 +189,6 @@
|
|||||||
|
|
||||||
- (void)dealloc
|
- (void)dealloc
|
||||||
{
|
{
|
||||||
[_actionHandle reset];
|
|
||||||
[_faceDetectorDisposable dispose];
|
[_faceDetectorDisposable dispose];
|
||||||
[_thumbnailsDisposable dispose];
|
[_thumbnailsDisposable dispose];
|
||||||
}
|
}
|
||||||
@ -255,11 +248,6 @@
|
|||||||
|
|
||||||
void(^toolbarDoneLongPressed)(id) = ^(id sender)
|
void(^toolbarDoneLongPressed)(id) = ^(id sender)
|
||||||
{
|
{
|
||||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
|
||||||
if (strongSelf == nil)
|
|
||||||
return;
|
|
||||||
|
|
||||||
[strongSelf doneButtonLongPressed:sender];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void(^toolbarTabPressed)(TGPhotoEditorTab) = ^(TGPhotoEditorTab tab)
|
void(^toolbarTabPressed)(TGPhotoEditorTab) = ^(TGPhotoEditorTab tab)
|
||||||
@ -2270,45 +2258,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)doneButtonLongPressed:(UIButton *)sender
|
|
||||||
{
|
|
||||||
if (_intent == TGPhotoEditorControllerVideoIntent)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_menuContainerView != nil)
|
|
||||||
{
|
|
||||||
[_menuContainerView removeFromSuperview];
|
|
||||||
_menuContainerView = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
_menuContainerView = [[TGMenuContainerView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.view.frame.size.width, self.view.frame.size.height)];
|
|
||||||
[self.view addSubview:_menuContainerView];
|
|
||||||
|
|
||||||
NSMutableArray *actions = [[NSMutableArray alloc] init];
|
|
||||||
[actions addObject:@{ @"title": @"Save to Camera Roll", @"action": @"save" }];
|
|
||||||
if ([_context canOpenURL:[NSURL URLWithString:@"instagram://"]])
|
|
||||||
[actions addObject:@{ @"title": @"Share on Instagram", @"action": @"instagram" }];
|
|
||||||
|
|
||||||
[_menuContainerView.menuView setButtonsAndActions:actions watcherHandle:_actionHandle];
|
|
||||||
[_menuContainerView.menuView sizeToFit];
|
|
||||||
|
|
||||||
CGRect titleLockIconViewFrame = [sender.superview convertRect:sender.frame toView:_menuContainerView];
|
|
||||||
titleLockIconViewFrame.origin.y += 16.0f;
|
|
||||||
[_menuContainerView showMenuFromRect:titleLockIconViewFrame animated:false];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)actionStageActionRequested:(NSString *)action options:(id)options
|
|
||||||
{
|
|
||||||
if ([action isEqualToString:@"menuAction"])
|
|
||||||
{
|
|
||||||
NSString *menuAction = options[@"action"];
|
|
||||||
if ([menuAction isEqualToString:@"save"])
|
|
||||||
[self _saveToCameraRoll];
|
|
||||||
else if ([menuAction isEqualToString:@"instagram"])
|
|
||||||
[self _openInInstagram];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - External Export
|
#pragma mark - External Export
|
||||||
|
|
||||||
- (void)_saveToCameraRoll
|
- (void)_saveToCameraRoll
|
||||||
|
@ -77,7 +77,7 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
|
|||||||
if (dictionary[@"originalSize"])
|
if (dictionary[@"originalSize"])
|
||||||
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
|
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
|
||||||
if (dictionary[@"entitiesData"]) {
|
if (dictionary[@"entitiesData"]) {
|
||||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"] hasAnimation:[dictionary[@"hasAnimation"] boolValue]];
|
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"] hasAnimation:[dictionary[@"hasAnimation"] boolValue] stickers:dictionary[@"stickersData"]];
|
||||||
} else if (dictionary[@"paintingImagePath"]) {
|
} else if (dictionary[@"paintingImagePath"]) {
|
||||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
|
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ swift_library(
|
|||||||
"//submodules/RingBuffer:RingBuffer",
|
"//submodules/RingBuffer:RingBuffer",
|
||||||
"//submodules/YuvConversion:YuvConversion",
|
"//submodules/YuvConversion:YuvConversion",
|
||||||
"//submodules/Utils/RangeSet:RangeSet",
|
"//submodules/Utils/RangeSet:RangeSet",
|
||||||
|
"//submodules/TextFormat:TextFormat",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -3,6 +3,7 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import RangeSet
|
import RangeSet
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
public enum MediaPlayerScrubbingNodeCap {
|
public enum MediaPlayerScrubbingNodeCap {
|
||||||
case square
|
case square
|
||||||
@ -29,6 +30,39 @@ public struct MediaPlayerScrubbingChapter: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func parseMediaPlayerChapters(_ string: NSAttributedString) -> [MediaPlayerScrubbingChapter] {
|
||||||
|
var existingTimecodes = Set<Double>()
|
||||||
|
var timecodeRanges: [(NSRange, TelegramTimecode)] = []
|
||||||
|
var lineRanges: [NSRange] = []
|
||||||
|
string.enumerateAttributes(in: NSMakeRange(0, string.length), options: [], using: { attributes, range, _ in
|
||||||
|
if let timecode = attributes[NSAttributedString.Key(TelegramTextAttributes.Timecode)] as? TelegramTimecode {
|
||||||
|
if !existingTimecodes.contains(timecode.time) {
|
||||||
|
timecodeRanges.append((range, timecode))
|
||||||
|
existingTimecodes.insert(timecode.time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
(string.string as NSString).enumerateSubstrings(in: NSMakeRange(0, string.length), options: .byLines, using: { _, range, _, _ in
|
||||||
|
lineRanges.append(range)
|
||||||
|
})
|
||||||
|
|
||||||
|
var chapters: [MediaPlayerScrubbingChapter] = []
|
||||||
|
for (timecodeRange, timecode) in timecodeRanges {
|
||||||
|
inner: for lineRange in lineRanges {
|
||||||
|
if lineRange.contains(timecodeRange.location) {
|
||||||
|
if lineRange.length > timecodeRange.length && timecodeRange.location < lineRange.location + 4 {
|
||||||
|
var title = ((string.string as NSString).substring(with: lineRange) as NSString).replacingCharacters(in: NSMakeRange(timecodeRange.location - lineRange.location, timecodeRange.length), with: "")
|
||||||
|
title = title.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
|
||||||
|
chapters.append(MediaPlayerScrubbingChapter(title: title, start: timecode.time))
|
||||||
|
}
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
private final class MediaPlayerScrubbingNodeButton: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||||
var beginScrubbing: (() -> Void)?
|
var beginScrubbing: (() -> Void)?
|
||||||
var endScrubbing: ((Bool) -> Void)?
|
var endScrubbing: ((Bool) -> Void)?
|
||||||
|
@ -847,7 +847,12 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 {
|
var isFallback = false
|
||||||
|
if case let .image(_, _, _, _, _, _, _, _, _, _, isFallbackValue) = rawEntry {
|
||||||
|
isFallback = isFallbackValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback {
|
||||||
let title: String
|
let title: String
|
||||||
if let _ = rawEntry.videoRepresentations.last {
|
if let _ = rawEntry.videoRepresentations.last {
|
||||||
title = self.presentationData.strings.ProfilePhoto_SetMainVideo
|
title = self.presentationData.strings.ProfilePhoto_SetMainVideo
|
||||||
@ -907,11 +912,14 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .image(_, reference, _, _, _, _, _, messageId, _, _, _):
|
case let .image(_, reference, _, _, _, _, _, messageId, _, _, isFallback):
|
||||||
if self.peer.id == self.context.account.peerId {
|
if self.peer.id == self.context.account.peerId {
|
||||||
if let reference = reference {
|
if isFallback {
|
||||||
|
let _ = self.context.engine.accountData.updateFallbackPhoto(resource: nil, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { _, _ in .single([:]) }).start()
|
||||||
|
} else if let reference = reference {
|
||||||
let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start()
|
let _ = self.context.engine.accountData.removeAccountPhoto(reference: reference).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry == self.entries.first {
|
if entry == self.entries.first {
|
||||||
dismiss = true
|
dismiss = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -107,12 +107,14 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
|
|||||||
var buttonText: String?
|
var buttonText: String?
|
||||||
var canShare = true
|
var canShare = true
|
||||||
switch entry {
|
switch entry {
|
||||||
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, _):
|
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback):
|
||||||
if date != 0 {
|
if date != 0 || isFallback {
|
||||||
nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
||||||
}
|
}
|
||||||
if let date = date, date != 0 {
|
if let date = date, date != 0 {
|
||||||
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string
|
dateText = humanReadableStringForTimestamp(strings: self.strings, dateTimeFormat: self.dateTimeFormat, timestamp: date).string
|
||||||
|
} else if isFallback {
|
||||||
|
dateText = !videoRepresentations.isEmpty ? self.strings.ProfilePhoto_PublicVideo : self.strings.ProfilePhoto_PublicPhoto
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!videoRepresentations.isEmpty) {
|
if (!videoRepresentations.isEmpty) {
|
||||||
|
@ -1595,7 +1595,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let isPremium = state?.isPremium == true
|
let isPremium = state?.isPremium == true
|
||||||
|
|
||||||
var dismissImpl: (() -> Void)?
|
var dismissImpl: (() -> Void)?
|
||||||
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "–").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium)
|
let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium)
|
||||||
controller.action = { [weak state] in
|
controller.action = { [weak state] in
|
||||||
dismissImpl?()
|
dismissImpl?()
|
||||||
if state?.isPremium == false {
|
if state?.isPremium == false {
|
||||||
@ -1611,23 +1611,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
updateIsFocused(true)
|
updateIsFocused(true)
|
||||||
|
|
||||||
// let controller = PremiumDemoScreen(
|
|
||||||
// context: accountContext,
|
|
||||||
// subject: demoSubject,
|
|
||||||
// source: .intro(state?.price),
|
|
||||||
// order: state?.configuration.perks,
|
|
||||||
// action: {
|
|
||||||
// if state?.isPremium == false {
|
|
||||||
// buy()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// controller.disposed = {
|
|
||||||
// updateIsFocused(false)
|
|
||||||
// }
|
|
||||||
// present(controller)
|
|
||||||
// updateIsFocused(true)
|
|
||||||
|
|
||||||
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
|
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
@ -628,7 +628,6 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
|
|
||||||
self.wrappingView = UIView()
|
self.wrappingView = UIView()
|
||||||
self.containerView = UIView()
|
self.containerView = UIView()
|
||||||
// self.scrollView = UIScrollView()
|
|
||||||
self.backgroundView = ComponentHostView()
|
self.backgroundView = ComponentHostView()
|
||||||
self.pagerView = ComponentHostView()
|
self.pagerView = ComponentHostView()
|
||||||
self.closeView = ComponentHostView()
|
self.closeView = ComponentHostView()
|
||||||
@ -637,9 +636,6 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
// self.scrollView.delegate = self
|
|
||||||
// self.scrollView.showsVerticalScrollIndicator = false
|
|
||||||
|
|
||||||
self.containerView.clipsToBounds = true
|
self.containerView.clipsToBounds = true
|
||||||
self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||||
|
|
||||||
@ -651,7 +647,6 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
self.containerView.addSubview(self.pagerView)
|
self.containerView.addSubview(self.pagerView)
|
||||||
self.containerView.addSubnode(self.footerNode)
|
self.containerView.addSubnode(self.footerNode)
|
||||||
self.containerView.addSubview(self.closeView)
|
self.containerView.addSubview(self.closeView)
|
||||||
// self.scrollView.addSubview(self.hostView)
|
|
||||||
|
|
||||||
self.footerNode.action = { [weak self] in
|
self.footerNode.action = { [weak self] in
|
||||||
self?.controller?.action()
|
self?.controller?.action()
|
||||||
@ -899,7 +894,6 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
transition.setFrame(view: self.containerView, frame: clipFrame)
|
transition.setFrame(view: self.containerView, frame: clipFrame)
|
||||||
// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
|
|
||||||
|
|
||||||
var clipLayout = layout.withUpdatedSize(clipFrame.size)
|
var clipLayout = layout.withUpdatedSize(clipFrame.size)
|
||||||
if case .regular = layout.metrics.widthClass {
|
if case .regular = layout.metrics.widthClass {
|
||||||
@ -914,10 +908,12 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updated(transition: Transition) {
|
func updated(transition: Transition) {
|
||||||
guard let controller = self.controller, let layout = self.currentLayout else {
|
guard let controller = self.controller else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let contentSize = self.containerView.bounds.size
|
||||||
|
|
||||||
let backgroundSize = self.backgroundView.update(
|
let backgroundSize = self.backgroundView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
@ -929,9 +925,9 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
])
|
])
|
||||||
),
|
),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: layout.size.width, height: layout.size.width)
|
containerSize: CGSize(width: contentSize.width, height: contentSize.width)
|
||||||
)
|
)
|
||||||
self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
|
self.backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize)
|
||||||
|
|
||||||
var isStandalone = false
|
var isStandalone = false
|
||||||
if case .other = controller.source {
|
if case .other = controller.source {
|
||||||
@ -1215,9 +1211,9 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: layout.size.width, height: self.containerView.frame.height)
|
containerSize: contentSize
|
||||||
)
|
)
|
||||||
self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize)
|
self.pagerView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentSize.width - pagerSize.width) / 2.0), y: 0.0), size: pagerSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1229,7 +1225,6 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
self.cachedCloseImage = closeImage
|
self.cachedCloseImage = closeImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let closeSize = self.closeView.update(
|
let closeSize = self.closeView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
@ -1259,7 +1254,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||||
)
|
)
|
||||||
self.closeView.frame = CGRect(origin: CGPoint(x: layout.size.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize)
|
self.closeView.frame = CGRect(origin: CGPoint(x: contentSize.width - closeSize.width * 1.5, y: 28.0 - closeSize.height / 2.0), size: closeSize)
|
||||||
}
|
}
|
||||||
private var cachedCloseImage: UIImage?
|
private var cachedCloseImage: UIImage?
|
||||||
|
|
||||||
|
@ -824,6 +824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
strongSelf.push(controller)
|
strongSelf.push(controller)
|
||||||
return true
|
return true
|
||||||
case let .suggestedProfilePhoto(image):
|
case let .suggestedProfilePhoto(image):
|
||||||
|
strongSelf.chatDisplayNode.dismissInput()
|
||||||
if let image = image {
|
if let image = image {
|
||||||
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
|
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
|
||||||
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||||
|
@ -106,6 +106,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
private let scrubberNode: MediaPlayerScrubbingNode
|
private let scrubberNode: MediaPlayerScrubbingNode
|
||||||
private let leftDurationLabel: MediaPlayerTimeTextNode
|
private let leftDurationLabel: MediaPlayerTimeTextNode
|
||||||
private let rightDurationLabel: MediaPlayerTimeTextNode
|
private let rightDurationLabel: MediaPlayerTimeTextNode
|
||||||
|
private let infoNode: ASTextNode
|
||||||
|
|
||||||
private let backwardButton: IconButtonNode
|
private let backwardButton: IconButtonNode
|
||||||
private let forwardButton: IconButtonNode
|
private let forwardButton: IconButtonNode
|
||||||
@ -149,6 +150,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
private var scrubbingDisposable: Disposable?
|
private var scrubbingDisposable: Disposable?
|
||||||
private var leftDurationLabelPushed = false
|
private var leftDurationLabelPushed = false
|
||||||
private var rightDurationLabelPushed = false
|
private var rightDurationLabelPushed = false
|
||||||
|
private var infoNodePushed = false
|
||||||
|
|
||||||
private var currentDuration: Double = 0.0
|
private var currentDuration: Double = 0.0
|
||||||
private var currentPosition: Double = 0.0
|
private var currentPosition: Double = 0.0
|
||||||
@ -196,6 +198,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
self.rightDurationLabel.alignment = .right
|
self.rightDurationLabel.alignment = .right
|
||||||
self.rightDurationLabel.keepPreviousValueOnEmptyState = true
|
self.rightDurationLabel.keepPreviousValueOnEmptyState = true
|
||||||
|
|
||||||
|
self.infoNode = ASTextNode()
|
||||||
|
self.infoNode.maximumNumberOfLines = 1
|
||||||
|
self.infoNode.isUserInteractionEnabled = false
|
||||||
|
self.infoNode.displaysAsynchronously = false
|
||||||
|
|
||||||
self.rateButton = HighlightableButtonNode()
|
self.rateButton = HighlightableButtonNode()
|
||||||
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
|
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
|
||||||
self.rateButton.displaysAsynchronously = false
|
self.rateButton.displaysAsynchronously = false
|
||||||
@ -238,6 +245,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.addSubnode(self.leftDurationLabel)
|
self.addSubnode(self.leftDurationLabel)
|
||||||
self.addSubnode(self.rightDurationLabel)
|
self.addSubnode(self.rightDurationLabel)
|
||||||
|
self.addSubnode(self.infoNode)
|
||||||
self.addSubnode(self.rateButton)
|
self.addSubnode(self.rateButton)
|
||||||
self.addSubnode(self.scrubberNode)
|
self.addSubnode(self.scrubberNode)
|
||||||
|
|
||||||
@ -283,16 +291,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
let leftDurationLabelPushed: Bool
|
let leftDurationLabelPushed: Bool
|
||||||
let rightDurationLabelPushed: Bool
|
let rightDurationLabelPushed: Bool
|
||||||
|
let infoNodePushed: Bool
|
||||||
if let value = value {
|
if let value = value {
|
||||||
leftDurationLabelPushed = value < 0.16
|
leftDurationLabelPushed = value < 0.16
|
||||||
rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
|
rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
|
||||||
|
infoNodePushed = value >= 0.16 && value <= 0.84
|
||||||
} else {
|
} else {
|
||||||
leftDurationLabelPushed = false
|
leftDurationLabelPushed = false
|
||||||
rightDurationLabelPushed = false
|
rightDurationLabelPushed = false
|
||||||
|
infoNodePushed = false
|
||||||
}
|
}
|
||||||
if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed {
|
if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed {
|
||||||
strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
|
strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
|
||||||
strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
|
strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
|
||||||
|
strongSelf.infoNodePushed = infoNodePushed
|
||||||
|
|
||||||
if let layout = strongSelf.validLayout {
|
if let layout = strongSelf.validLayout {
|
||||||
let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring))
|
let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring))
|
||||||
@ -778,6 +790,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
|||||||
let rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0
|
let rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0
|
||||||
transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
|
transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
|
||||||
|
|
||||||
|
let infoLabelVerticalOffset: CGFloat = self.infoNodePushed ? 6.0 : 0.0
|
||||||
|
|
||||||
|
let infoSize = self.infoNode.measure(CGSize(width: width - 60.0 * 2.0 - 100.0, height: 100.0))
|
||||||
|
self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize)
|
||||||
|
transition.updatePosition(node: self.infoNode, position: CGPoint(x: width / 2.0, y: scrubberVerticalOrigin + 14.0 + infoLabelVerticalOffset + infoSize.height / 2.0))
|
||||||
|
|
||||||
|
|
||||||
let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
|
let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
|
||||||
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0)))
|
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset), size: CGSize(width: 24.0, height: 24.0)))
|
||||||
|
|
||||||
|
@ -1377,17 +1377,33 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
|
|||||||
interaction.suggestPhoto()
|
interaction.suggestPhoto()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemCustom, text: presentationData.strings.UserInfo_SetCustomPhoto(compactName).string, color: .accent, icon: UIImage(bundleImageName: "Settings/SetAvatar"), action: {
|
let setText: String
|
||||||
|
if user.photo.first?.isPersonal == true {
|
||||||
|
setText = presentationData.strings.UserInfo_ChangeCustomPhoto(compactName).string
|
||||||
|
} else {
|
||||||
|
setText = presentationData.strings.UserInfo_SetCustomPhoto(compactName).string
|
||||||
|
}
|
||||||
|
|
||||||
|
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemCustom, text: setText, color: .accent, icon: UIImage(bundleImageName: "Settings/SetAvatar"), action: {
|
||||||
interaction.setCustomPhoto()
|
interaction.setCustomPhoto()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if user.photo.first?.isPersonal == true || state.updatingAvatar != nil {
|
if user.photo.first?.isPersonal == true || state.updatingAvatar != nil {
|
||||||
var representation: TelegramMediaImageRepresentation?
|
var representation: TelegramMediaImageRepresentation?
|
||||||
|
var originalIsVideo: Bool?
|
||||||
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.photo {
|
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.photo {
|
||||||
representation = photo?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28))
|
representation = photo?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28))
|
||||||
|
originalIsVideo = !(photo?.videoRepresentations.isEmpty ?? true)
|
||||||
}
|
}
|
||||||
|
|
||||||
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: presentationData.strings.UserInfo_ResetCustomPhoto, color: .accent, icon: nil, iconSignal: peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)), action: {
|
let removeText: String
|
||||||
|
if let originalIsVideo {
|
||||||
|
removeText = originalIsVideo ? presentationData.strings.UserInfo_ResetCustomVideo : presentationData.strings.UserInfo_ResetCustomPhoto
|
||||||
|
} else {
|
||||||
|
removeText = user.photo.first?.hasVideo == true ? presentationData.strings.UserInfo_RemoveCustomVideo : presentationData.strings.UserInfo_RemoveCustomPhoto
|
||||||
|
}
|
||||||
|
|
||||||
|
items[.peerDataSettings]!.append(PeerInfoScreenActionItem(id: ItemReset, text: removeText, color: .accent, icon: nil, iconSignal: peerAvatarCompleteImage(account: context.account, peer: EnginePeer(user), forceProvidedRepresentation: true, representation: representation, size: CGSize(width: 28.0, height: 28.0)), action: {
|
||||||
interaction.resetCustomPhoto()
|
interaction.resetCustomPhoto()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -3488,11 +3504,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPersonal = false
|
||||||
var currentIsVideo = false
|
var currentIsVideo = false
|
||||||
let item = strongSelf.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
|
let item = strongSelf.headerNode.avatarListNode.listContainerNode.currentItemNode?.item
|
||||||
if let item = item, case let .image(_, _, videoRepresentations, _, _) = item {
|
if let item = item, case let .image(_, representations, videoRepresentations, _, _) = item {
|
||||||
|
if representations.first?.representation.isPersonal == true {
|
||||||
|
isPersonal = true
|
||||||
|
}
|
||||||
currentIsVideo = !videoRepresentations.isEmpty
|
currentIsVideo = !videoRepresentations.isEmpty
|
||||||
}
|
}
|
||||||
|
guard !isPersonal else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let items: [ContextMenuItem] = [
|
let items: [ContextMenuItem] = [
|
||||||
.action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in
|
.action(ContextMenuActionItem(text: currentIsVideo ? strongSelf.presentationData.strings.PeerInfo_ReportProfileVideo : strongSelf.presentationData.strings.PeerInfo_ReportProfilePhoto, icon: { theme in
|
||||||
|
@ -36,6 +36,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 {
|
|||||||
case webBrowserSettings = 16
|
case webBrowserSettings = 16
|
||||||
case intentsSettings = 17
|
case intentsSettings = 17
|
||||||
case translationSettings = 18
|
case translationSettings = 18
|
||||||
|
case drawingSettings = 19
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ApplicationSpecificSharedDataKeys {
|
public struct ApplicationSpecificSharedDataKeys {
|
||||||
@ -58,6 +59,7 @@ public struct ApplicationSpecificSharedDataKeys {
|
|||||||
public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue)
|
public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue)
|
||||||
public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue)
|
public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue)
|
||||||
public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue)
|
public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue)
|
||||||
|
public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user