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.SetCustomPhoto" = "Set Photo for %@";
|
||||
"UserInfo.ChangeCustomPhoto" = "Change Photo for %@";
|
||||
"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.SuggestPhotoTitle" = "Do you want to suggest a profile picture for %@?";
|
||||
@ -8538,3 +8542,20 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Attachment.EnableSpoiler" = "Hide With 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 metrics: LayoutMetrics
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let orientation: UIInterfaceOrientation?
|
||||
public let isVisible: Bool
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
@ -40,6 +41,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
inputHeight: CGFloat,
|
||||
metrics: LayoutMetrics,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
orientation: UIInterfaceOrientation? = nil,
|
||||
isVisible: Bool,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
@ -52,6 +54,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
self.inputHeight = inputHeight
|
||||
self.metrics = metrics
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.orientation = orientation
|
||||
self.isVisible = isVisible
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -82,6 +85,9 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
if lhs.orientation != rhs.orientation {
|
||||
return false
|
||||
}
|
||||
if lhs.isVisible != rhs.isVisible {
|
||||
return false
|
||||
}
|
||||
|
@ -276,6 +276,18 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -296,6 +308,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingBubbleEntity else {
|
||||
@ -305,6 +319,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -316,6 +332,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
@ -358,6 +375,8 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
@ -367,7 +386,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
|
||||
@ -10,7 +11,7 @@ public protocol DrawingEntity: AnyObject {
|
||||
|
||||
var lineWidth: CGFloat { get set }
|
||||
var color: DrawingColor { get set }
|
||||
|
||||
|
||||
func duplicate() -> DrawingEntity
|
||||
|
||||
var currentEntityView: DrawingEntityView? { get }
|
||||
@ -127,25 +128,58 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
private(set) var selectedEntityView: DrawingEntityView?
|
||||
|
||||
public var getEntityCenterPosition: () -> CGPoint = { return .zero }
|
||||
public var getEntityInitialRotation: () -> CGFloat = { return 0.0 }
|
||||
public var hasSelectionChanged: (Bool) -> Void = { _ in }
|
||||
var selectionChanged: (DrawingEntity?) -> 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) {
|
||||
self.context = context
|
||||
self.size = size
|
||||
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(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) {
|
||||
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] = []
|
||||
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) {
|
||||
let entities = codableEntities.map { $0.entity }
|
||||
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)
|
||||
} else {
|
||||
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 {
|
||||
var occupied = false
|
||||
@ -214,8 +248,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
|
||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
let center = self.startPosition(relativeTo: relativeTo)
|
||||
let rotation = self.getEntityInitialRotation()
|
||||
|
||||
if let shape = entity as? DrawingSimpleShapeEntity {
|
||||
shape.position = center
|
||||
shape.rotation = rotation
|
||||
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
@ -237,12 +274,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
} else if let sticker = entity as? DrawingStickerEntity {
|
||||
sticker.position = center
|
||||
sticker.rotation = rotation
|
||||
if setup {
|
||||
sticker.referenceDrawingSize = self.size
|
||||
sticker.scale = 1.0
|
||||
}
|
||||
} else if let bubble = entity as? DrawingBubbleEntity {
|
||||
bubble.position = center
|
||||
bubble.rotation = rotation
|
||||
if setup {
|
||||
let size = self.newEntitySize()
|
||||
bubble.referenceDrawingSize = self.size
|
||||
@ -251,6 +290,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
} else if let text = entity as? DrawingTextEntity {
|
||||
text.position = center
|
||||
text.rotation = rotation
|
||||
if setup {
|
||||
text.referenceDrawingSize = self.size
|
||||
text.width = floor(self.size.width * 0.9)
|
||||
@ -260,11 +300,45 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add(_ entity: DrawingEntity) -> DrawingEntityView {
|
||||
func add(_ entity: DrawingEntity, announce: Bool = true) -> DrawingEntityView {
|
||||
let view = entity.makeView(context: self.context)
|
||||
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()
|
||||
self.addSubview(view)
|
||||
|
||||
if announce {
|
||||
self.entityAdded(entity)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
@ -279,15 +353,35 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return newEntity
|
||||
}
|
||||
|
||||
func remove(uuid: UUID) {
|
||||
func remove(uuid: UUID, animated: Bool = false, announce: Bool = true) {
|
||||
if let view = self.getView(for: uuid) {
|
||||
if self.selectedEntityView === view {
|
||||
self.selectedEntityView?.removeFromSuperview()
|
||||
self.selectedEntityView = nil
|
||||
self.selectionChanged(nil)
|
||||
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 containerView: DrawingEntitiesView?
|
||||
|
||||
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||
var onSnapToYAxis: (Bool) -> Void = { _ in }
|
||||
|
||||
init(context: AccountContext, entity: DrawingEntity) {
|
||||
self.context = context
|
||||
self.entity = entity
|
||||
|
@ -17,21 +17,50 @@ import ViewControllerComponent
|
||||
import ContextUI
|
||||
import ChatEntityKeyboardInputNode
|
||||
import EntityKeyboard
|
||||
import TelegramUIPreferences
|
||||
|
||||
enum DrawingToolState: Equatable {
|
||||
enum Key: CaseIterable {
|
||||
case pen
|
||||
case arrow
|
||||
case marker
|
||||
case neon
|
||||
case eraser
|
||||
case blur
|
||||
enum DrawingToolState: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case brushState
|
||||
case eraserState
|
||||
}
|
||||
|
||||
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 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 {
|
||||
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
|
||||
|
||||
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 {
|
||||
return EraserState(size: size)
|
||||
}
|
||||
@ -122,6 +169,53 @@ enum DrawingToolState: Equatable {
|
||||
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 {
|
||||
@ -148,6 +242,13 @@ struct DrawingState: Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedTools(_ tools: [DrawingToolState]) -> DrawingState {
|
||||
return DrawingState(
|
||||
selectedTool: self.selectedTool,
|
||||
tools: tools
|
||||
)
|
||||
}
|
||||
|
||||
func withUpdatedColor(_ color: DrawingColor) -> DrawingState {
|
||||
var tools = self.tools
|
||||
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 let sourceView: UIView
|
||||
|
||||
@ -217,6 +348,7 @@ private let toolsTag = GenericComponentViewTag()
|
||||
private let modeTag = GenericComponentViewTag()
|
||||
private let flipButtonTag = GenericComponentViewTag()
|
||||
private let fillButtonTag = GenericComponentViewTag()
|
||||
private let zoomOutButtonTag = GenericComponentViewTag()
|
||||
private let textSettingsTag = GenericComponentViewTag()
|
||||
private let sizeSliderTag = GenericComponentViewTag()
|
||||
private let color1Tag = GenericComponentViewTag()
|
||||
@ -379,7 +511,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
self.currentColor = self.drawingState.tools.first?.color ?? DrawingColor(rgb: 0xffffff)
|
||||
|
||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||
|
||||
|
||||
let stickerPickerInputData = self.stickerPickerInputData
|
||||
Queue.concurrentDefaultQueue().after(0.5, {
|
||||
let emojiItems = EmojiPagerContentComponent.emojiInputData(
|
||||
@ -432,6 +564,35 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
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 {
|
||||
@ -508,10 +669,12 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
func presentShapePicker(_ sourceView: UIView) {
|
||||
let strings = self.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Rectangle",
|
||||
text: strings.Paint_Rectangle,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeRectangle"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -523,7 +686,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Ellipse",
|
||||
text: strings.Paint_Ellipse,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeEllipse"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -535,7 +698,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Bubble",
|
||||
text: strings.Paint_Bubble,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeBubble"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -547,7 +710,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Star",
|
||||
text: strings.Paint_Star,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeStar"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -559,7 +722,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
.action(
|
||||
ContextMenuActionItem(
|
||||
text: "Arrow",
|
||||
text: strings.Paint_Arrow,
|
||||
icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/ShapeArrow"), color: theme.contextMenu.primaryColor)},
|
||||
action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
@ -669,13 +832,23 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
let state = context.state
|
||||
let controller = environment.controller
|
||||
|
||||
let strings = environment.strings
|
||||
|
||||
let previewBrushSize = component.previewBrushSize
|
||||
let performAction = component.performAction
|
||||
component.updateState.connect { [weak state] updatedState in
|
||||
state?.updateDrawingState(updatedState)
|
||||
}
|
||||
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
|
||||
state?.updateSelectedEntity(entity)
|
||||
@ -1175,14 +1348,14 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
ZoomOutButtonContent(
|
||||
title: "Zoom Out",
|
||||
title: strings.Paint_ZoomOut,
|
||||
image: state.image(.zoomOut)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
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),
|
||||
transition: .immediate
|
||||
)
|
||||
@ -1241,30 +1414,29 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
.opacity(isEditingText ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
if state.drawingViewState.canRedo && !isEditingText {
|
||||
let redoButton = redoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.redo))
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
let redoButton = redoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.redo))
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||
.opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let clearAllButton = clearAllButton.update(
|
||||
component: Button(
|
||||
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,
|
||||
action: {
|
||||
@ -1327,6 +1499,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
color = nil
|
||||
} else if state.selectedEntity is DrawingStickerEntity {
|
||||
color = nil
|
||||
} else if [.eraser, .blur].contains(state.drawingState.selectedTool) {
|
||||
color = nil
|
||||
} else {
|
||||
color = state.currentColor
|
||||
}
|
||||
@ -1413,7 +1587,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.done))
|
||||
),
|
||||
action: {
|
||||
action: { [weak state] in
|
||||
state?.saveToolState()
|
||||
apply.invoke(Void())
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
|
||||
@ -1456,7 +1631,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
|
||||
let modeAndSize = modeAndSize.update(
|
||||
component: ModeAndSizeComponent(
|
||||
values: ["Draw", "Sticker", "Text"],
|
||||
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
|
||||
sizeValue: selectedSize,
|
||||
isEditing: false,
|
||||
isEnabled: true,
|
||||
@ -1500,7 +1675,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
animatingOut = true
|
||||
}
|
||||
|
||||
let deselectEntity = component.deselectEntity
|
||||
let backButton = backButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
@ -1516,11 +1690,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let state = state {
|
||||
if let selectedEntity = state.selectedEntity, !(selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity) {
|
||||
deselectEntity.invoke(Void())
|
||||
} else {
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
state.saveToolState()
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
@ -1559,7 +1730,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
private var validLayout: ContainerViewLayout?
|
||||
private var validLayout: (ContainerViewLayout, UIInterfaceOrientation?)?
|
||||
|
||||
private var _drawingView: DrawingView?
|
||||
var drawingView: DrawingView {
|
||||
@ -1655,6 +1826,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
if self._entitiesView == nil, let controller = self.controller {
|
||||
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
|
||||
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
|
||||
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
|
||||
if let strongSelf = self, let controller = strongSelf.controller, let currentImage = controller.getCurrentImage() {
|
||||
@ -1699,7 +1876,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
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
|
||||
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 {
|
||||
@ -1711,7 +1888,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
}))
|
||||
}
|
||||
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 {
|
||||
strongSelf.entitiesView.bringToFront(uuid: entityView.entity.uuid)
|
||||
}
|
||||
@ -1975,8 +2152,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, animateOut: true, transition: .easeInOut(duration: 0.2))
|
||||
if let (layout, orientation) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: true, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
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.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) {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
self.validLayout = (layout, orientation)
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
@ -2071,6 +2253,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
inputHeight: layout.inputHeight ?? 0.0,
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
orientation: orientation,
|
||||
isVisible: true,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
@ -2195,8 +2378,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.style = nextStyle
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
toggleAlignment: { [weak self] in
|
||||
@ -2215,8 +2398,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.alignment = nextAlignment
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
updateFont: { [weak self] font in
|
||||
@ -2226,8 +2409,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textEntity.font = font.font
|
||||
entityView.update()
|
||||
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
if let (layout, orientation) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, orientation: orientation, transition: .immediate)
|
||||
}
|
||||
},
|
||||
toggleKeyboard: { [weak self] in
|
||||
@ -2300,8 +2483,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate)
|
||||
if let (layout, orientation) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, orientation: orientation, animateOut: false, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2350,10 +2533,6 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self, context: self.context)
|
||||
|
||||
@ -2404,7 +2583,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
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! {
|
||||
@ -2428,13 +2624,14 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
self.node.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
private var orientation: UIInterfaceOrientation?
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
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(
|
||||
size: size,
|
||||
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
|
||||
@ -2447,6 +2644,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
inputHeightIsInteractivellyChanging: false,
|
||||
inVoiceOver: false
|
||||
)
|
||||
self.orientation = orientation
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
let lineWidth = self.maxLineWidth * 0.5
|
||||
let expandedBounds = self.bounds.insetBy(dx: -lineWidth, dy: -lineWidth)
|
||||
@ -291,6 +296,18 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -311,9 +328,11 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@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
|
||||
}
|
||||
let isAspectLocked = [.star].contains(entity.shapeType)
|
||||
@ -321,6 +340,8 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -332,50 +353,54 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
self.currentHandle = self.layer
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedSize = entity.size
|
||||
var updatedPosition = entity.position
|
||||
|
||||
let minimumSize = entityView.minimumSize
|
||||
|
||||
if self.currentHandle === self.leftHandle {
|
||||
let deltaX = delta.x * cos(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.y -= deltaY * -0.5
|
||||
|
||||
if isAspectLocked {
|
||||
updatedSize.height -= delta.x
|
||||
updatedSize.height = updatedSize.width
|
||||
}
|
||||
} else if self.currentHandle === self.rightHandle {
|
||||
let deltaX = delta.x * cos(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.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.height += delta.x
|
||||
updatedSize.height = updatedSize.width
|
||||
}
|
||||
} else if self.currentHandle === self.topHandle {
|
||||
let deltaX = delta.y * sin(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.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width -= delta.y
|
||||
updatedSize.width = updatedSize.height
|
||||
}
|
||||
} else if self.currentHandle === self.bottomHandle {
|
||||
let deltaX = delta.y * sin(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.y += deltaY * 0.5
|
||||
if isAspectLocked {
|
||||
updatedSize.width += delta.y
|
||||
updatedSize.width = updatedSize.height
|
||||
}
|
||||
} else if self.currentHandle === self.topLeftHandle {
|
||||
var delta = delta
|
||||
@ -416,15 +441,19 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView,
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.size = updatedSize
|
||||
entity.position = updatedPosition
|
||||
entityView.update()
|
||||
entityView.update(animated: false)
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
||||
super.init(context: context, entity: entity)
|
||||
|
||||
self.addSubview(self.imageNode.view)
|
||||
|
||||
|
||||
self.setup()
|
||||
}
|
||||
|
||||
@ -340,6 +340,10 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
self.border.lineCap = .round
|
||||
self.border.fillColor = UIColor.clear.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)
|
||||
|
||||
for handle in handles {
|
||||
@ -356,6 +360,18 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToYAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -376,6 +392,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
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)
|
||||
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -397,7 +418,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedPosition = entity.position
|
||||
var updatedScale = entity.scale
|
||||
var updatedRotation = entity.rotation
|
||||
@ -419,6 +441,8 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.position = updatedPosition
|
||||
@ -428,7 +452,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -494,8 +520,121 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG
|
||||
self.leftHandle.position = CGPoint(x: 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.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 monospaced
|
||||
case round
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
case .sanFrancisco:
|
||||
self = .sanFrancisco
|
||||
case .newYork:
|
||||
self = .newYork
|
||||
case .monospaced:
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
}
|
||||
}
|
||||
case custom(String, String)
|
||||
}
|
||||
|
||||
enum Alignment: Codable {
|
||||
@ -101,17 +89,6 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
case center
|
||||
case right
|
||||
|
||||
init(font: DrawingTextEntity.Alignment) {
|
||||
switch font {
|
||||
case .left:
|
||||
self = .left
|
||||
case .center:
|
||||
self = .center
|
||||
case .right:
|
||||
self = .right
|
||||
}
|
||||
}
|
||||
|
||||
var alignment: NSTextAlignment {
|
||||
switch self {
|
||||
case .left:
|
||||
@ -567,7 +544,11 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
||||
|
||||
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 {
|
||||
case .sanFrancisco:
|
||||
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)
|
||||
case .round:
|
||||
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)
|
||||
self.textView.font = font
|
||||
|
||||
@ -761,6 +745,18 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.panGestureRecognizer = panGestureRecognizer
|
||||
|
||||
self.snapTool.onSnapXUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapTool.onSnapYUpdated = { [weak self] snapped in
|
||||
if let strongSelf = self, let entityView = strongSelf.entityView {
|
||||
entityView.onSnapToXAxis(snapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -784,16 +780,19 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
return true
|
||||
}
|
||||
|
||||
private let snapTool = DrawingEntitySnapTool()
|
||||
|
||||
private var currentHandle: CALayer?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
|
||||
|
||||
if let sublayers = self.layer.sublayers {
|
||||
for layer in sublayers {
|
||||
if layer.frame.contains(location) {
|
||||
@ -806,6 +805,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
case .changed:
|
||||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
let velocity = gestureRecognizer.velocity(in: entityView.superview)
|
||||
|
||||
var updatedScale = entity.scale
|
||||
var updatedPosition = entity.position
|
||||
@ -829,6 +829,8 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
} else if self.currentHandle === self.layer {
|
||||
updatedPosition.x += delta.x
|
||||
updatedPosition.y += delta.y
|
||||
|
||||
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition)
|
||||
}
|
||||
|
||||
entity.scale = updatedScale
|
||||
@ -838,7 +840,9 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
|
||||
gestureRecognizer.setTranslation(.zero, in: entityView)
|
||||
case .ended:
|
||||
break
|
||||
self.snapTool.reset()
|
||||
case .cancelled:
|
||||
self.snapTool.reset()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -903,10 +907,19 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
||||
|
||||
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
|
||||
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
|
||||
|
||||
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.lineDashPattern = [12.0 / self.scale as NSNumber, 12.0 / self.scale as NSNumber]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let uuid = UUID()
|
||||
|
||||
@ -784,3 +686,106 @@ final class EraserTool: DrawingElement {
|
||||
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)
|
||||
}
|
||||
|
||||
enum DrawingCommand {
|
||||
enum DrawingElementTransform {
|
||||
case move(offset: CGPoint)
|
||||
}
|
||||
|
||||
case addStroke(DrawingElement)
|
||||
case updateStrokes([UUID], DrawingElementTransform)
|
||||
case removeStroke(DrawingElement)
|
||||
case addEntity(DrawingEntity)
|
||||
case updateEntity(UUID, DrawingEntity)
|
||||
enum DrawingOperation {
|
||||
case element(DrawingElement)
|
||||
case addEntity(UUID)
|
||||
case removeEntity(DrawingEntity)
|
||||
case updateEntityZOrder(UUID, Int32)
|
||||
}
|
||||
|
||||
public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDrawingView {
|
||||
@ -63,10 +55,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
case pen
|
||||
case marker
|
||||
case neon
|
||||
case pencil
|
||||
case eraser
|
||||
case lasso
|
||||
case objectRemover
|
||||
case blur
|
||||
}
|
||||
|
||||
@ -82,7 +72,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
var getFullImage: (Bool) -> UIImage? = { _ in return nil }
|
||||
|
||||
private var elements: [DrawingElement] = []
|
||||
private var redoElements: [DrawingElement] = []
|
||||
private var undoStack: [DrawingOperation] = []
|
||||
private var redoStack: [DrawingOperation] = []
|
||||
fileprivate var uncommitedElement: DrawingElement?
|
||||
|
||||
private(set) var drawingImage: UIImage?
|
||||
@ -109,6 +100,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
private var strokeRecognitionTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var isDrawing = false
|
||||
private var drawingGestureStartTimestamp: Double?
|
||||
|
||||
private func loadTemplates() {
|
||||
func load(_ name: String) {
|
||||
@ -134,7 +126,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
load("shape_arrow")
|
||||
}
|
||||
|
||||
public init(size: CGSize) {
|
||||
init(size: CGSize) {
|
||||
self.imageSize = size
|
||||
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
@ -203,20 +195,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if case .objectRemover = 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 .lasso = strongSelf.tool {
|
||||
if case let .smoothCurve(bezierPath) = path {
|
||||
let scale = strongSelf.bounds.width / strongSelf.imageSize.width
|
||||
|
||||
@ -254,6 +233,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
case .began:
|
||||
strongSelf.isDrawing = true
|
||||
strongSelf.previousStrokePoint = nil
|
||||
strongSelf.drawingGestureStartTimestamp = CACurrentMediaTime()
|
||||
|
||||
if strongSelf.uncommitedElement != nil {
|
||||
strongSelf.finishDrawing()
|
||||
@ -263,7 +243,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
return
|
||||
}
|
||||
|
||||
if newElement is MarkerTool || newElement is PencilTool {
|
||||
if newElement is MarkerTool {
|
||||
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 let previousStrokePoint = strongSelf.previousStrokePoint, line.points.count > 10 {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if lastPoint.location.distance(to: previousStrokePoint) > 10.0 {
|
||||
strongSelf.previousStrokePoint = lastPoint.location
|
||||
|
||||
@ -290,7 +271,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
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
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -298,31 +279,30 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
if let previousStrokePoint = strongSelf.previousStrokePoint, lastPoint.location.distance(to: previousStrokePoint) <= 10.0 {
|
||||
let strokeRecognizer = Unistroke(points: line.points.map { $0.location })
|
||||
if let template = strokeRecognizer.match(templates: strongSelf.loadedTemplates, minThreshold: 0.5) {
|
||||
|
||||
let edges = line.bounds
|
||||
let bounds = CGRect(origin: edges.origin, size: CGSize(width: edges.width - edges.minX, height: edges.height - edges.minY))
|
||||
|
||||
var entity: DrawingEntity?
|
||||
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.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||
entity = shapeEntity
|
||||
} 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.position = bounds.center
|
||||
shapeEntity.size = bounds.size
|
||||
shapeEntity.size = CGSize(width: bounds.size.width * 1.1, height: bounds.size.height * 1.1)
|
||||
entity = shapeEntity
|
||||
} 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.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
|
||||
} 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.start = line.points.first?.location ?? .zero
|
||||
arrowEntity.end = line.points[line.points.count - 4].location
|
||||
@ -331,6 +311,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
|
||||
if let entity = entity {
|
||||
strongSelf.entitiesView?.add(entity)
|
||||
strongSelf.entitiesView?.selectEntity(entity)
|
||||
strongSelf.cancelDrawing()
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = false
|
||||
strongSelf.drawingGesturePipeline?.gestureRecognizer?.isEnabled = true
|
||||
@ -561,9 +542,10 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
let complete: (Bool) -> Void = { synchronous in
|
||||
self.commit(interactive: true, synchronous: synchronous)
|
||||
|
||||
self.redoElements.removeAll()
|
||||
self.redoStack.removeAll()
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
self.elements.append(uncommitedElement)
|
||||
self.undoStack.append(.element(uncommitedElement))
|
||||
self.uncommitedElement = nil
|
||||
}
|
||||
|
||||
@ -584,7 +566,8 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
|
||||
self.uncommitedElement = nil
|
||||
self.elements.removeAll()
|
||||
self.redoElements.removeAll()
|
||||
self.undoStack.removeAll()
|
||||
self.redoStack.removeAll()
|
||||
|
||||
let snapshotView = UIImageView(image: self.drawingImage)
|
||||
snapshotView.frame = self.bounds
|
||||
@ -605,38 +588,89 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
}
|
||||
|
||||
private func undo() {
|
||||
guard let lastElement = self.elements.last else {
|
||||
guard let lastOperation = self.undoStack.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.redoElements.append(lastElement)
|
||||
self.elements.removeLast()
|
||||
|
||||
let snapshotView = UIImageView(image: self.drawingImage)
|
||||
snapshotView.frame = self.bounds
|
||||
self.addSubview(snapshotView)
|
||||
self.commit(reset: true)
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
switch lastOperation {
|
||||
case let .element(element):
|
||||
self.uncommitedElement = nil
|
||||
self.redoStack.append(.element(element))
|
||||
self.elements.removeAll(where: { $0.uuid == element.uuid })
|
||||
|
||||
let snapshotView = UIImageView(image: self.drawingImage)
|
||||
snapshotView.frame = self.bounds
|
||||
self.addSubview(snapshotView)
|
||||
self.commit(reset: true)
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
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()
|
||||
}
|
||||
|
||||
private func redo() {
|
||||
guard let lastElement = self.redoElements.last else {
|
||||
guard let lastOperation = self.redoStack.last else {
|
||||
return
|
||||
}
|
||||
self.uncommitedElement = nil
|
||||
self.elements.append(lastElement)
|
||||
self.redoElements.removeLast()
|
||||
self.uncommitedElement = lastElement
|
||||
|
||||
self.commit(reset: false)
|
||||
self.uncommitedElement = nil
|
||||
|
||||
switch lastOperation {
|
||||
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()
|
||||
}
|
||||
@ -723,9 +757,9 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
|
||||
private func updateInternalState() {
|
||||
self.stateUpdated(NavigationState(
|
||||
canUndo: !self.elements.isEmpty,
|
||||
canRedo: !self.redoElements.isEmpty,
|
||||
canClear: !self.elements.isEmpty,
|
||||
canUndo: !self.elements.isEmpty || !self.undoStack.isEmpty,
|
||||
canRedo: !self.redoStack.isEmpty,
|
||||
canClear: !self.elements.isEmpty || !(self.entitiesView?.entities.isEmpty ?? true),
|
||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
||||
isDrawing: self.isDrawing
|
||||
))
|
||||
@ -764,15 +798,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
||||
lineWidth: self.toolBrushSize * scale,
|
||||
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:
|
||||
let blurTool = BlurTool(
|
||||
drawingSize: self.imageSize,
|
||||
|
@ -4,20 +4,46 @@ import Display
|
||||
|
||||
final class PenTool: DrawingElement {
|
||||
class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
var segmentsCount = 0
|
||||
|
||||
var displayLink: ConstantDisplayLinkAnimator?
|
||||
func setup(size: CGSize) {
|
||||
self.shouldRasterize = true
|
||||
self.contentsScale = 1.0
|
||||
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
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 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.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))
|
||||
}
|
||||
|
||||
@ -48,7 +74,7 @@ final class PenTool: DrawingElement {
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
@ -239,6 +265,7 @@ private class StrokeLine {
|
||||
let d: CGPoint
|
||||
let abWidth: CGFloat
|
||||
let cdWidth: CGFloat
|
||||
let rect: CGRect
|
||||
}
|
||||
|
||||
struct Point {
|
||||
@ -281,8 +308,8 @@ private class StrokeLine {
|
||||
return appendPoint(point)
|
||||
}
|
||||
|
||||
func drawInContext(_ context: CGContext) {
|
||||
self.drawSegments(self.segments, inContext: context)
|
||||
func drawInContext(_ context: CGContext, upTo: Int? = nil) {
|
||||
self.drawSegments(self.segments, upTo: upTo ?? self.segments.count, inContext: context)
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
func drawSegments(_ segments: [Segment], inContext context: CGContext) {
|
||||
for segment in segments {
|
||||
var count: Int {
|
||||
return self.segments.count
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
//let color = [UIColor.red, UIColor.green, UIColor.blue, UIColor.yellow].randomElement()!
|
||||
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
|
@ -44,11 +44,12 @@ enum DrawingTextAlignment: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
enum DrawingTextFont: Equatable, CaseIterable {
|
||||
enum DrawingTextFont: Equatable, Hashable {
|
||||
case sanFrancisco
|
||||
case newYork
|
||||
case monospaced
|
||||
case round
|
||||
case custom(String, String)
|
||||
|
||||
init(font: DrawingTextEntity.Font) {
|
||||
switch font {
|
||||
@ -60,6 +61,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
self = .monospaced
|
||||
case .round:
|
||||
self = .round
|
||||
case let .custom(font, name):
|
||||
self = .custom(font, name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +76,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
return .monospaced
|
||||
case .round:
|
||||
return .round
|
||||
case let .custom(font, name):
|
||||
return .custom(font, name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +91,8 @@ enum DrawingTextFont: Equatable, CaseIterable {
|
||||
return "Monospaced"
|
||||
case .round:
|
||||
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)
|
||||
case .round:
|
||||
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
|
||||
|
||||
var validIds = Set<DrawingTextFont>()
|
||||
for value in component.values {
|
||||
validIds.insert(value)
|
||||
|
||||
contentWidth += 12.0
|
||||
let button: HighlightableButton
|
||||
if let current = self.buttons[value] {
|
||||
@ -387,6 +399,13 @@ final class TextFontComponent: Component {
|
||||
}
|
||||
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 {
|
||||
self.scrollView.contentSize = CGSize(width: contentWidth, height: 30.0)
|
||||
}
|
||||
@ -590,6 +609,16 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
if component.color != nil {
|
||||
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(
|
||||
component: TextFontComponent(
|
||||
@ -617,7 +646,7 @@ final class TextSettingsComponent: CombinedComponent {
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
),
|
||||
values: DrawingTextFont.allCases,
|
||||
values: fonts,
|
||||
selectedValue: component.font,
|
||||
tag: component.tag,
|
||||
updated: { font in
|
||||
|
@ -1012,43 +1012,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
|
||||
func setupItem(_ item: UniversalVideoGalleryItem) {
|
||||
if self.item?.content.id != item.content.id {
|
||||
func parseChapters(_ 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
|
||||
}
|
||||
|
||||
var chapters = parseChapters(item.caption)
|
||||
if self.item?.content.id != item.content.id {
|
||||
var chapters = parseMediaPlayerChapters(item.caption)
|
||||
if chapters.isEmpty, let description = item.description {
|
||||
chapters = parseChapters(description)
|
||||
chapters = parseMediaPlayerChapters(description)
|
||||
}
|
||||
let scrubberView = ChatVideoGalleryItemScrubberView(chapters: chapters)
|
||||
self.scrubberView = scrubberView
|
||||
@ -2710,7 +2677,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
let baseNavigationController = strongSelf.baseNavigationController()
|
||||
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 {
|
||||
let animateInAsReplacement = false
|
||||
switch action {
|
||||
|
@ -19,9 +19,9 @@
|
||||
|
||||
@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;
|
||||
|
||||
|
@ -75,6 +75,9 @@
|
||||
|
||||
@protocol TGPhotoDrawingEntitiesView <NSObject>
|
||||
|
||||
@property (nonatomic, copy) CGPoint (^getEntityCenterPosition)(void);
|
||||
@property (nonatomic, copy) CGFloat (^getEntityInitialRotation)(void);
|
||||
|
||||
@property (nonatomic, copy) void(^hasSelectionChanged)(bool);
|
||||
@property (nonatomic, readonly) BOOL hasSelection;
|
||||
|
||||
@ -107,6 +110,7 @@
|
||||
safeInsets:(UIEdgeInsets)safeInsets
|
||||
statusBarHeight:(CGFloat)statusBarHeight
|
||||
inputHeight:(CGFloat)inputHeight
|
||||
orientation:(UIInterfaceOrientation)orientation
|
||||
animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
@ -158,12 +158,24 @@
|
||||
if (strongController == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
if (strongSelf.willFinishWithVideo != nil) {
|
||||
strongSelf.willFinishWithVideo(image, ^{
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image, asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
});
|
||||
} else {
|
||||
if (strongSelf.didFinishWithVideo != nil)
|
||||
strongSelf.didFinishWithVideo(image,
|
||||
asset, adjustments);
|
||||
|
||||
commit();
|
||||
|
||||
[strongController dismissAnimated:false];
|
||||
}
|
||||
};
|
||||
[itemViews addObject:carouselItem];
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
@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];
|
||||
paintingData->_drawingData = data;
|
||||
@ -28,14 +28,16 @@
|
||||
paintingData->_stillImage = stillImage;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
paintingData->_hasAnimation = hasAnimation;
|
||||
paintingData->_stickers = stickers;
|
||||
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];
|
||||
paintingData->_imagePath = imagePath;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
paintingData->_hasAnimation = hasAnimation;
|
||||
paintingData->_stickers = stickers;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
@ -51,6 +53,7 @@
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_entitiesData = _entitiesData;
|
||||
paintingData->_hasAnimation = _hasAnimation;
|
||||
paintingData->_stickers = _stickers;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
@ -122,18 +125,6 @@
|
||||
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
|
||||
{
|
||||
if (object == self)
|
||||
|
@ -219,6 +219,22 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
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];
|
||||
}
|
||||
|
||||
@ -771,6 +787,7 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
safeInsets:UIEdgeInsetsMake(0.0, _context.safeAreaInset.left, 0.0, _context.safeAreaInset.right)
|
||||
statusBarHeight:[_context statusBarFrame].size.height
|
||||
inputHeight:_keyboardHeight
|
||||
orientation:self.effectiveOrientation
|
||||
animated:animated];
|
||||
|
||||
}
|
||||
@ -878,6 +895,16 @@ const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
return UIRectEdgeTop | UIRectEdgeBottom;
|
||||
}
|
||||
|
||||
- (CGPoint)entityCenterPoint
|
||||
{
|
||||
return [_previewView convertPoint:TGPaintCenterOfRect(_previewView.bounds) toView:_entitiesView];
|
||||
}
|
||||
|
||||
- (CGFloat)entityInitialRotation
|
||||
{
|
||||
return TGCounterRotationForOrientation(_photoEditor.cropOrientation) - _photoEditor.cropRotation;
|
||||
}
|
||||
|
||||
+ (CGSize)maximumPaintingSize
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
|
@ -4,8 +4,6 @@
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import <LegacyComponents/ASWatcher.h>
|
||||
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
#import <LegacyComponents/TGPhotoEditorAnimation.h>
|
||||
@ -52,7 +50,7 @@
|
||||
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
|
||||
#import "TGCameraCapturedVideo.h"
|
||||
|
||||
@interface TGPhotoEditorController () <ASWatcher, TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||
@interface TGPhotoEditorController () <TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||
{
|
||||
bool _switchingTab;
|
||||
TGPhotoEditorTab _availableTabs;
|
||||
@ -102,7 +100,6 @@
|
||||
bool _hasOpenedPhotoTools;
|
||||
bool _hiddenToolbarView;
|
||||
|
||||
TGMenuContainerView *_menuContainerView;
|
||||
UIDocumentInteractionController *_documentController;
|
||||
|
||||
bool _dismissed;
|
||||
@ -136,16 +133,13 @@
|
||||
|
||||
@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
|
||||
{
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
{
|
||||
_context = context;
|
||||
_actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true];
|
||||
|
||||
|
||||
self.automaticallyManageScrollViewInsets = false;
|
||||
self.autoManageStatusBarBackground = false;
|
||||
self.isImportant = true;
|
||||
@ -195,7 +189,6 @@
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[_actionHandle reset];
|
||||
[_faceDetectorDisposable dispose];
|
||||
[_thumbnailsDisposable dispose];
|
||||
}
|
||||
@ -255,11 +248,6 @@
|
||||
|
||||
void(^toolbarDoneLongPressed)(id) = ^(id sender)
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[strongSelf doneButtonLongPressed:sender];
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
- (void)_saveToCameraRoll
|
||||
|
@ -77,7 +77,7 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
|
||||
if (dictionary[@"originalSize"])
|
||||
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
|
||||
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"]) {
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ swift_library(
|
||||
"//submodules/RingBuffer:RingBuffer",
|
||||
"//submodules/YuvConversion:YuvConversion",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -3,6 +3,7 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import RangeSet
|
||||
import TextFormat
|
||||
|
||||
public enum MediaPlayerScrubbingNodeCap {
|
||||
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 {
|
||||
var beginScrubbing: (() -> Void)?
|
||||
var endScrubbing: ((Bool) -> Void)?
|
||||
|
@ -846,8 +846,13 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
||||
self?.dismissImmediately()
|
||||
})
|
||||
}))
|
||||
|
||||
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 {
|
||||
if self.peer.id == self.context.account.peerId, let position = rawEntry.indexData?.position, position > 0 || isFallback {
|
||||
let title: String
|
||||
if let _ = rawEntry.videoRepresentations.last {
|
||||
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 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()
|
||||
}
|
||||
|
||||
if entry == self.entries.first {
|
||||
dismiss = true
|
||||
} else {
|
||||
|
@ -107,12 +107,14 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
|
||||
var buttonText: String?
|
||||
var canShare = true
|
||||
switch entry {
|
||||
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, _):
|
||||
if date != 0 {
|
||||
case let .image(_, _, _, videoRepresentations, peer, date, _, _, _, _, isFallback):
|
||||
if date != 0 || isFallback {
|
||||
nameText = peer.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""
|
||||
}
|
||||
if let date = date, date != 0 {
|
||||
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) {
|
||||
|
@ -1595,7 +1595,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
let isPremium = state?.isPremium == true
|
||||
|
||||
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
|
||||
dismissImpl?()
|
||||
if state?.isPremium == false {
|
||||
@ -1611,23 +1611,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
}
|
||||
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])
|
||||
}
|
||||
))
|
||||
|
@ -628,7 +628,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
|
||||
self.wrappingView = UIView()
|
||||
self.containerView = UIView()
|
||||
// self.scrollView = UIScrollView()
|
||||
self.backgroundView = ComponentHostView()
|
||||
self.pagerView = ComponentHostView()
|
||||
self.closeView = ComponentHostView()
|
||||
@ -636,10 +635,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
self.footerNode = FooterNode(theme: self.presentationData.theme, title: buttonTitle, gloss: gloss)
|
||||
|
||||
super.init()
|
||||
|
||||
// self.scrollView.delegate = self
|
||||
// self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
|
||||
self.containerView.clipsToBounds = true
|
||||
self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
@ -651,8 +647,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
self.containerView.addSubview(self.pagerView)
|
||||
self.containerView.addSubnode(self.footerNode)
|
||||
self.containerView.addSubview(self.closeView)
|
||||
// self.scrollView.addSubview(self.hostView)
|
||||
|
||||
|
||||
self.footerNode.action = { [weak self] in
|
||||
self?.controller?.action()
|
||||
}
|
||||
@ -889,7 +884,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
} else {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
self.containerView.layer.cornerRadius = 10.0
|
||||
|
||||
|
||||
let verticalInset: CGFloat = 44.0
|
||||
|
||||
let maxSide = max(layout.size.width, layout.size.height)
|
||||
@ -899,7 +894,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
@ -914,10 +908,12 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
}
|
||||
|
||||
func updated(transition: Transition) {
|
||||
guard let controller = self.controller, let layout = self.currentLayout else {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentSize = self.containerView.bounds.size
|
||||
|
||||
let backgroundSize = self.backgroundView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
@ -929,9 +925,9 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
])
|
||||
),
|
||||
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
|
||||
if case .other = controller.source {
|
||||
@ -1215,9 +1211,9 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
)
|
||||
),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1228,7 +1224,6 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
|
||||
self.cachedCloseImage = closeImage
|
||||
}
|
||||
|
||||
|
||||
let closeSize = self.closeView.update(
|
||||
transition: .immediate,
|
||||
@ -1259,7 +1254,7 @@ public class PremiumLimitsListScreen: ViewController {
|
||||
environment: {},
|
||||
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?
|
||||
|
||||
|
@ -824,6 +824,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.push(controller)
|
||||
return true
|
||||
case let .suggestedProfilePhoto(image):
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
if let image = image {
|
||||
if message.effectivelyIncoming(strongSelf.context.account.peerId) {
|
||||
var selectedNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
|
||||
|
@ -106,6 +106,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private let scrubberNode: MediaPlayerScrubbingNode
|
||||
private let leftDurationLabel: MediaPlayerTimeTextNode
|
||||
private let rightDurationLabel: MediaPlayerTimeTextNode
|
||||
private let infoNode: ASTextNode
|
||||
|
||||
private let backwardButton: IconButtonNode
|
||||
private let forwardButton: IconButtonNode
|
||||
@ -149,6 +150,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
private var scrubbingDisposable: Disposable?
|
||||
private var leftDurationLabelPushed = false
|
||||
private var rightDurationLabelPushed = false
|
||||
private var infoNodePushed = false
|
||||
|
||||
private var currentDuration: Double = 0.0
|
||||
private var currentPosition: Double = 0.0
|
||||
@ -196,6 +198,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
self.rightDurationLabel.alignment = .right
|
||||
self.rightDurationLabel.keepPreviousValueOnEmptyState = true
|
||||
|
||||
self.infoNode = ASTextNode()
|
||||
self.infoNode.maximumNumberOfLines = 1
|
||||
self.infoNode.isUserInteractionEnabled = false
|
||||
self.infoNode.displaysAsynchronously = false
|
||||
|
||||
self.rateButton = HighlightableButtonNode()
|
||||
self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
|
||||
self.rateButton.displaysAsynchronously = false
|
||||
@ -238,6 +245,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
|
||||
self.addSubnode(self.leftDurationLabel)
|
||||
self.addSubnode(self.rightDurationLabel)
|
||||
self.addSubnode(self.infoNode)
|
||||
self.addSubnode(self.rateButton)
|
||||
self.addSubnode(self.scrubberNode)
|
||||
|
||||
@ -283,16 +291,20 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
}
|
||||
let leftDurationLabelPushed: Bool
|
||||
let rightDurationLabelPushed: Bool
|
||||
let infoNodePushed: Bool
|
||||
if let value = value {
|
||||
leftDurationLabelPushed = value < 0.16
|
||||
rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
|
||||
infoNodePushed = value >= 0.16 && value <= 0.84
|
||||
} else {
|
||||
leftDurationLabelPushed = 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.rightDurationLabelPushed = rightDurationLabelPushed
|
||||
strongSelf.infoNodePushed = infoNodePushed
|
||||
|
||||
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))
|
||||
@ -778,6 +790,13 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
|
||||
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)))
|
||||
|
||||
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)
|
||||
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()
|
||||
}))
|
||||
|
||||
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()
|
||||
}))
|
||||
|
||||
if user.photo.first?.isPersonal == true || state.updatingAvatar != nil {
|
||||
var representation: TelegramMediaImageRepresentation?
|
||||
var originalIsVideo: Bool?
|
||||
if let cachedData = data.cachedData as? CachedUserData, case let .known(photo) = cachedData.photo {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
@ -3488,11 +3504,18 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
return
|
||||
}
|
||||
|
||||
var isPersonal = false
|
||||
var currentIsVideo = false
|
||||
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
|
||||
}
|
||||
guard !isPersonal else {
|
||||
return
|
||||
}
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.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 intentsSettings = 17
|
||||
case translationSettings = 18
|
||||
case drawingSettings = 19
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificSharedDataKeys {
|
||||
@ -58,6 +59,7 @@ public struct ApplicationSpecificSharedDataKeys {
|
||||
public static let webBrowserSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.webBrowserSettings.rawValue)
|
||||
public static let intentsSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.intentsSettings.rawValue)
|
||||
public static let translationSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.translationSettings.rawValue)
|
||||
public static let drawingSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.drawingSettings.rawValue)
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user