Various improvements

This commit is contained in:
Ilya Laktyushin 2022-12-21 02:08:39 +04:00
parent 5fe2e3f69b
commit b5c35cd8e7
30 changed files with 1151 additions and 465 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]];
}

View File

@ -20,6 +20,7 @@ swift_library(
"//submodules/RingBuffer:RingBuffer",
"//submodules/YuvConversion:YuvConversion",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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