diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 9a101f2359..7550088fd0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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"; + diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 156f4aab63..9d29d934b2 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -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 } diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index bfc16122a9..d221c5649b 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -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 } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 4ff9650d66..e1dea2974a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -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 diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 5ea8066224..c0b7a3bb0e 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -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) } } diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index 905773081c..bc381e977e 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -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 } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 71af63bd1d..115e66ae66 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -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 + } +} diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 5ef8e8b946..c8d80d6498 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -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 +}() diff --git a/submodules/DrawingUI/Sources/DrawingTools.swift b/submodules/DrawingUI/Sources/DrawingTools.swift index 24f2756743..78ad6a612a 100644 --- a/submodules/DrawingUI/Sources/DrawingTools.swift +++ b/submodules/DrawingUI/Sources/DrawingTools.swift @@ -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) +// } +// } +//} diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index bf2290da8a..1ed75daa24 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -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, diff --git a/submodules/DrawingUI/Sources/PenTool.swift b/submodules/DrawingUI/Sources/PenTool.swift index 216106d2a1..2dd561a70b 100644 --- a/submodules/DrawingUI/Sources/PenTool.swift +++ b/submodules/DrawingUI/Sources/PenTool.swift @@ -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) diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index 915f755365..7d2dcde162 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -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() 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 diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index a6689b104a..8d646bd36b 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -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() - 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 { diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h index aa1af97d5f..62e1d434d9 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPaintingData.h @@ -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; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index a9074cc8b1..cb8023f5ab 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -75,6 +75,9 @@ @protocol TGPhotoDrawingEntitiesView +@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 diff --git a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m index c9e5f6205b..a91456c19c 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaAvatarMenuMixin.m @@ -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]; diff --git a/submodules/LegacyComponents/Sources/TGPaintingData.m b/submodules/LegacyComponents/Sources/TGPaintingData.m index 78e9132564..8b3800a901 100644 --- a/submodules/LegacyComponents/Sources/TGPaintingData.m +++ b/submodules/LegacyComponents/Sources/TGPaintingData.m @@ -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) diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m index 398db8cc93..45f8c272e1 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m @@ -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; diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index a22d710fe6..c313ee669e 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -4,8 +4,6 @@ #import -#import - #import #import @@ -52,7 +50,7 @@ #import #import "TGCameraCapturedVideo.h" -@interface TGPhotoEditorController () +@interface TGPhotoEditorController () { 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)context item:(id)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id)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 diff --git a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m index 1e58f1171d..4c8b2d5e36 100644 --- a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m +++ b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m @@ -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"]]; } diff --git a/submodules/MediaPlayer/BUILD b/submodules/MediaPlayer/BUILD index 5dc2c5bc7e..318c28c7c5 100644 --- a/submodules/MediaPlayer/BUILD +++ b/submodules/MediaPlayer/BUILD @@ -20,6 +20,7 @@ swift_library( "//submodules/RingBuffer:RingBuffer", "//submodules/YuvConversion:YuvConversion", "//submodules/Utils/RangeSet:RangeSet", + "//submodules/TextFormat:TextFormat", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index 8448d70ddb..0bcd163973 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -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() + 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)? diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 0aa21649e4..68fcd353ed 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -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 { diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index de52914c8c..6278a700e1 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -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) { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index fd17c6cde9..c1b5fab349 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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]) } )) diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 70155ed74e..676672d5ef 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -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? diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7beeccfded..7fb091cbba 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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?))? diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 0e52bef025..7376f7efbe 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -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))) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 1d2fea029a..c599bcf134 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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 diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index da4427fe3f..db42fb3204 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -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 {