diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index 108213a0bd..2b6b32ab63 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -106,6 +106,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/Camera", "//submodules/TelegramUI/Components/DustEffect", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", ], visibility = [ "//visibility:public", diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 2e7f76740d..b996d290e1 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3082,7 +3082,6 @@ public final class DrawingToolsInteraction { return } - var isRectangleImage = false var isVideo = false var isAdditional = false var isMessage = false @@ -3090,8 +3089,6 @@ public final class DrawingToolsInteraction { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true isAdditional = isAdditionalValue - } else if case let .image(_, type) = entity.content, case .rectangle = type { - isRectangleImage = true } else if case .message = entity.content { isMessage = true } @@ -3154,7 +3151,7 @@ public final class DrawingToolsInteraction { } - if #available(iOS 17.0, *), isRectangleImage { + if #available(iOS 17.0, *), let stickerEntity = entityView.entity as? DrawingStickerEntity, stickerEntity.canCutOut { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content { let _ = (cutoutStickerImage(from: image) diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 2b44455647..cd4bb1e43b 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -14,6 +14,7 @@ import UniversalMediaPlayer import TelegramPresentationData import TelegramUniversalVideoContent import DustEffect +import DynamicCornerRadiusView private class BlurView: UIVisualEffectView { private func setup() { @@ -66,7 +67,9 @@ public class DrawingStickerEntityView: DrawingEntityView { let imageNode: TransformImageNode var animationNode: DefaultAnimatedStickerNodeImpl? var videoNode: UniversalVideoNode? + var videoMaskView: DynamicCornerRadiusView? var animatedImageView: UIImageView? + var overlayImageView: UIImageView? var cameraPreviewView: UIView? let progressDisposable = MetaDisposable() @@ -154,7 +157,7 @@ public class DrawingStickerEntityView: DrawingEntityView { return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: return CGSize(width: 512.0, height: 512.0) - case let .message(_, _, size): + case let .message(_, size, _, _, _): return size } } @@ -276,14 +279,17 @@ public class DrawingStickerEntityView: DrawingEntityView { self.animatedImageView = imageView self.addSubview(imageView) self.setNeedsLayout() - } else if case .message = self.stickerEntity.content { + } else if case let .message(_, _, file, mediaRect, _) = self.stickerEntity.content { if let image = self.stickerEntity.renderImage { - self.setupWithImage(image) + self.setupWithImage(image, overlayImage: self.stickerEntity.overlayRenderImage) + } + if let file, let _ = mediaRect { + self.setupWithVideo(file) } } } - private func setupWithImage(_ image: UIImage) { + private func setupWithImage(_ image: UIImage, overlayImage: UIImage? = nil) { let imageView: UIImageView if let current = self.animatedImageView { imageView = current @@ -294,6 +300,20 @@ public class DrawingStickerEntityView: DrawingEntityView { self.animatedImageView = imageView } imageView.image = image + + if let overlayImage { + let imageView: UIImageView + if let current = self.overlayImageView { + imageView = current + } else { + imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + self.addSubview(imageView) + self.overlayImageView = imageView + } + imageView.image = overlayImage + } + self.currentSize = nil self.setNeedsLayout() } @@ -335,6 +355,9 @@ public class DrawingStickerEntityView: DrawingEntityView { videoNode.isUserInteractionEnabled = false videoNode.clipsToBounds = true self.addSubnode(videoNode) + if let overlayImageView = self.overlayImageView { + self.addSubview(overlayImageView) + } self.videoNode = videoNode self.setNeedsLayout() videoNode.play() @@ -579,21 +602,53 @@ public class DrawingStickerEntityView: DrawingEntityView { } if let videoNode = self.videoNode { - var imageSize = imageSize - if case let .message(_, file, _) = self.stickerEntity.content, let dimensions = file?.dimensions { - let fittedDimensions = dimensions.cgSize.aspectFitted(boundingSize) - imageSize = fittedDimensions - videoNode.cornerRadius = 0.0 + if case let .message(_, size, _, rect, cornerRadius) = self.stickerEntity.content, let rect, let cornerRadius { + let baseSize = self.stickerEntity.baseSize + let scale = baseSize.width / size.width + let scaledRect = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale) + videoNode.frame = scaledRect + videoNode.updateLayout(size: scaledRect.size, transition: .immediate) + + if cornerRadius > 100.0 { + videoNode.cornerRadius = cornerRadius * scale + } else { + videoNode.cornerRadius = 0.0 + + let hasRoundBottomCorners = scaledRect.maxY > baseSize.height - 6.0 + if hasRoundBottomCorners { + let maskView: DynamicCornerRadiusView + if let current = self.videoMaskView { + maskView = current + } else { + maskView = DynamicCornerRadiusView() + self.videoMaskView = maskView + videoNode.view.mask = maskView + } + + let corners = DynamicCornerRadiusView.Corners( + minXMinY: 0.0, + maxXMinY: 0.0, + minXMaxY: cornerRadius * scale, + maxXMaxY: cornerRadius * scale + ) + maskView.update(size: scaledRect.size, corners: corners, transition: .immediate) + } else { + videoNode.view.mask = nil + } + } } else { videoNode.cornerRadius = floor(imageSize.width * 0.03) + videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) + videoNode.updateLayout(size: imageSize, transition: .immediate) } - videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) - videoNode.updateLayout(size: imageSize, transition: .immediate) } if let animatedImageView = self.animatedImageView { animatedImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) } + if let overlayImageView = self.overlayImageView { + overlayImageView.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize) + } if let cameraPreviewView = self.cameraPreviewView { cameraPreviewView.layer.cornerRadius = imageSize.width / 2.0 @@ -717,11 +772,42 @@ public class DrawingStickerEntityView: DrawingEntityView { } func getRenderSubEntities() -> [DrawingEntity] { - guard case let .message(_, file, _) = self.stickerEntity.content else { - return [] + if case let .message(_, _, file, _, cornerRadius) = self.stickerEntity.content { + if let file, let cornerRadius, let videoNode = self.videoNode { + let _ = cornerRadius + let stickerSize = self.bounds.size + let stickerPosition = self.stickerEntity.position + let videoSize = videoNode.frame.size + let scale = self.stickerEntity.scale + let rotation = self.stickerEntity.rotation + + let videoPosition = videoNode.position.offsetBy(dx: -stickerSize.width / 2.0, dy: -stickerSize.height / 2.0) + let videoScale = videoSize.width / stickerSize.width + + let videoEntity = DrawingStickerEntity(content: .video(file)) + videoEntity.referenceDrawingSize = self.stickerEntity.referenceDrawingSize + videoEntity.position = stickerPosition.offsetBy( + dx: (videoPosition.x * cos(rotation) - videoPosition.y * sin(rotation)) * scale, + dy: (videoPosition.y * cos(rotation) + videoPosition.x * sin(rotation)) * scale + ) + videoEntity.scale = scale * videoScale + videoEntity.rotation = rotation + + var entities: [DrawingEntity] = [] + entities.append(videoEntity) + + if let overlayImage = self.stickerEntity.overlayRenderImage { + let overlayEntity = DrawingStickerEntity(content: .image(overlayImage, .sticker)) + overlayEntity.referenceDrawingSize = self.stickerEntity.referenceDrawingSize + overlayEntity.position = self.stickerEntity.position + overlayEntity.scale = self.stickerEntity.scale + overlayEntity.rotation = self.stickerEntity.rotation + entities.append(overlayEntity) + } + + return entities + } } - - let _ = file return [] } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index b88e7e4a58..3f9f06740e 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -26,10 +26,10 @@ private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selec accentColor = UIColor(rgb: 0x999999) } context.setStrokeColor(accentColor.cgColor) - lineWidth = 2.0 + lineWidth = 2.0 - UIScreenPixel } else { context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor) - lineWidth = 1.0 + lineWidth = 1.0 - UIScreenPixel } if bordered || selected { @@ -95,10 +95,13 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem { } } +private let badgeSize = CGSize(width: 24.0, height: 24.0) +private let badgeStrokeSize: CGFloat = 2.0 + private final class ThemeSettingsAppIconNode : ASDisplayNode { private let iconNode: ASImageNode private let overlayNode: ASImageNode - private let lockNode: ASImageNode + fileprivate let lockNode: ASImageNode private let textNode: ImmediateTextNode private var action: (() -> Void)? @@ -108,11 +111,11 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { override init() { self.iconNode = ASImageNode() - self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0)) + self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.iconNode.isLayerBacked = true self.overlayNode = ASImageNode() - self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0)) + self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 63.0, height: 63.0)) self.overlayNode.isLayerBacked = true self.lockNode = ASImageNode() @@ -141,7 +144,7 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { self.iconNode.image = icon self.textNode.attributedText = title self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected) - self.lockNode.image = locked ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: color) : nil + self.lockNode.isHidden = !locked self.action = { action() } @@ -172,26 +175,25 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode { super.layout() let bounds = self.bounds + let iconSize = CGSize(width: 63.0, height: 63.0) - self.iconNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0)) - self.overlayNode.frame = CGRect(origin: CGPoint(x: 9.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0)) + self.iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - iconSize.width) / 2.0), y: 13.0), size: iconSize) + self.overlayNode.frame = self.iconNode.frame let textSize = self.textNode.updateLayout(bounds.size) - var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width)) / 2.0, y: 87.0), size: textSize) - if self.locked { - textFrame = textFrame.offsetBy(dx: 5.0, dy: 0.0) - } + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: 81.0), size: textSize) self.textNode.frame = textFrame - self.lockNode.frame = CGRect(x: self.textNode.frame.minX - 10.0, y: 90.0, width: 6.0, height: 8.0) + let badgeFinalSize = CGSize(width: badgeSize.width + badgeStrokeSize * 2.0, height: badgeSize.height + badgeStrokeSize * 2.0) + self.lockNode.frame = CGRect(x: bounds.width - 24.0, y: 4.0, width: badgeFinalSize.width, height: badgeFinalSize.height) - self.activateAreaNode.frame = self.bounds + self.activateAreaNode.frame = bounds } } private let textFont = Font.regular(12.0) -private let selectedTextFont = Font.bold(12.0) +private let selectedTextFont = Font.medium(12.0) class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode @@ -199,7 +201,7 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode - private let scrollNode: ASScrollNode + private let containerNode: ASDisplayNode private var nodes: [ThemeSettingsAppIconNode] = [] private var item: ThemeSettingsAppIconItem? @@ -209,6 +211,8 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { return self.item?.tag } + private var lockImage: UIImage? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -221,35 +225,23 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { self.maskNode = ASImageNode() - self.scrollNode = ASScrollNode() + self.containerNode = ASDisplayNode() super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.scrollNode) + self.addSubnode(self.containerNode) } - - override func didLoad() { - super.didLoad() - self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true - self.scrollNode.view.showsHorizontalScrollIndicator = false - } - - private func scrollToNode(_ node: ThemeSettingsAppIconNode, animated: Bool) { - let bounds = self.scrollNode.view.bounds - let frame = node.frame.insetBy(dx: -48.0, dy: 0.0) - - if frame.minX < bounds.minX || frame.maxX > bounds.maxX { - self.scrollNode.view.scrollRectToVisible(frame, animated: animated) - } - } - + func asyncLayout() -> (_ item: ThemeSettingsAppIconItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { return { item, params, neighbors in let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel - contentSize = CGSize(width: params.width, height: 116.0) + let nodeSize = CGSize(width: 74.0, height: 102.0) + let height: CGFloat = nodeSize.height * ceil(CGFloat(item.icons.count) / 4.0) + 12.0 + + contentSize = CGSize(width: params.width, height: height) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) @@ -257,10 +249,32 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { return (layout, { [weak self] in if let strongSelf = self { + let previousItem = strongSelf.item strongSelf.item = item strongSelf.layoutParams = params - strongSelf.scrollNode.view.contentInset = UIEdgeInsets() + if previousItem?.theme !== item.theme { + strongSelf.lockImage = generateImage(CGSize(width: badgeSize.width + badgeStrokeSize, height: badgeSize.height + badgeStrokeSize), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(item.theme.list.itemBlocksBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size)) + + context.addEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: badgeStrokeSize, dy: badgeStrokeSize)) + context.clip() + + var locations: [CGFloat] = [0.0, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + + if let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: .white) { + context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false) + } + }) + } + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor @@ -309,33 +323,37 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) - strongSelf.scrollNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height)) + strongSelf.containerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 2.0), size: CGSize(width: layoutSize.width - params.leftInset - params.rightInset, height: layoutSize.height)) - let nodeInset: CGFloat = 4.0 - let nodeSize = CGSize(width: 80.0, height: 112.0) - var nodeOffset = nodeInset + let sideInset: CGFloat = 8.0 + let spacing: CGFloat = floorToScreenPixels((params.width - sideInset * 2.0 - params.leftInset - params.rightInset - nodeSize.width * 4.0) / 3.0) + let verticalSpacing: CGFloat = 0.0 - var updated = false - var selectedNode: ThemeSettingsAppIconNode? + var x: CGFloat = sideInset + var y: CGFloat = 0.0 var i = 0 for icon in item.icons { + if i > 0 && i % 4 == 0 { + x = sideInset + y += nodeSize.height + verticalSpacing + } + let nodeFrame = CGRect(x: x, y: y, width: nodeSize.width, height: nodeSize.height) + x += nodeSize.width + spacing + let imageNode: ThemeSettingsAppIconNode if strongSelf.nodes.count > i { imageNode = strongSelf.nodes[i] } else { imageNode = ThemeSettingsAppIconNode() strongSelf.nodes.append(imageNode) - strongSelf.scrollNode.addSubnode(imageNode) - updated = true + strongSelf.containerNode.addSubnode(imageNode) } + imageNode.lockNode.image = strongSelf.lockImage if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) { let selected = icon.name == item.currentIconName - if selected { - selectedNode = imageNode - } - + var name = "Icon" var bordered = true switch icon.name { @@ -369,30 +387,15 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode { name = icon.name } - imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { [weak self, weak imageNode] in + imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { item.updated(icon) - if let imageNode = imageNode { - self?.scrollToNode(imageNode, animated: true) - } }) } - imageNode.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize) - nodeOffset += nodeSize.width + 15.0 + imageNode.frame = nodeFrame i += 1 } - - if let lastNode = strongSelf.nodes.last { - let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: strongSelf.scrollNode.frame.height) - if strongSelf.scrollNode.view.contentSize != contentSize { - strongSelf.scrollNode.view.contentSize = contentSize - } - } - - if updated, let selectedNode = selectedNode { - strongSelf.scrollToNode(selectedNode, animated: false) - } } }) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 72910fd8db..aeef94f3fa 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -134,7 +134,7 @@ public enum CodableDrawingEntity: Equatable { reaction: reaction, flags: flags ) - } else if case let .message(messageIds, _, _) = entity.content, let messageId = messageIds.first { + } else if case let .message(messageIds, _, _, _, _) = entity.content, let messageId = messageIds.first { return .channelMessage(coordinates: coordinates, messageId: messageId) } else { return nil diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 3f7b805d3b..3081218bc0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -33,7 +33,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case animatedImage(Data, UIImage) case video(TelegramMediaFile) case dualVideoReference(Bool) - case message([MessageId], TelegramMediaFile?, CGSize) + case message([MessageId], CGSize, TelegramMediaFile?, CGRect?, CGFloat?) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -67,9 +67,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } - case let .message(messageIds, innerFile, size): - if case .message(messageIds, innerFile, size) = rhs { - return true + case let .message(lhsMessageIds, lhsSize, lhsFile, lhsMediaFrame, lhsCornerRadius): + if case let .message(rhsMessageIds, rhsSize, rhsFile, rhsMediaFrame, rhsCornerRadius) = rhs { + return lhsMessageIds == rhsMessageIds && lhsSize == rhsSize && lhsFile?.fileId == rhsFile?.fileId && lhsMediaFrame == rhsMediaFrame && lhsCornerRadius == rhsCornerRadius } else { return false } @@ -89,14 +89,19 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case dualVideo case isAdditionalVideo case messageIds - case explicitSize + case messageFile + case messageSize + case messageMediaRect + case messageMediaCornerRadius case referenceDrawingSize case position case scale case rotation case mirrored case isExplicitlyStatic + case canCutOut case renderImage + case renderSubEntities } public var uuid: UUID @@ -116,12 +121,15 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var rotation: CGFloat public var mirrored: Bool + public var canCutOut = false + public var isExplicitlyStatic: Bool public var color: DrawingColor = DrawingColor.clear public var lineWidth: CGFloat = 0.0 public var secondaryRenderImage: UIImage? + public var overlayRenderImage: UIImage? public var center: CGPoint { return self.position @@ -146,7 +154,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case .dualVideoReference: dimensions = CGSize(width: 512.0, height: 512.0) - case let .message(_, _, size): + case let .message(_, size, _, _, _): dimensions = size } @@ -217,8 +225,11 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { - let size = try container.decodeIfPresent(CGSize.self, forKey: .explicitSize) ?? .zero - self.content = .message(messageIds, nil, size) + let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero + let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .messageFile) + let mediaRect = try container.decodeIfPresent(CGRect.self, forKey: .messageMediaRect) + let mediaCornerRadius = try container.decodeIfPresent(CGFloat.self, forKey: .messageMediaCornerRadius) + self.content = .message(messageIds, size, file, mediaRect, mediaCornerRadius) } else if let _ = try container.decodeIfPresent(Bool.self, forKey: .dualVideo) { let isAdditional = try container.decodeIfPresent(Bool.self, forKey: .isAdditionalVideo) ?? false self.content = .dualVideoReference(isAdditional) @@ -260,9 +271,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.mirrored = try container.decode(Bool.self, forKey: .mirrored) self.isExplicitlyStatic = try container.decodeIfPresent(Bool.self, forKey: .isExplicitlyStatic) ?? false + self.canCutOut = try container.decodeIfPresent(Bool.self, forKey: .canCutOut) ?? false + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { self.renderImage = UIImage(data: renderImageData) } + if let renderSubEntities = try? container.decodeIfPresent([CodableDrawingEntity].self, forKey: .renderSubEntities) { + self.renderSubEntities = renderSubEntities.compactMap { $0.entity as? DrawingStickerEntity } + } } public func encode(to encoder: Encoder) throws { @@ -313,10 +329,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case let .dualVideoReference(isAdditional): try container.encode(true, forKey: .dualVideo) try container.encode(isAdditional, forKey: .isAdditionalVideo) - case let .message(messageIds, innerFile, size): + case let .message(messageIds, size, file, mediaRect, mediaCornerRadius): try container.encode(messageIds, forKey: .messageIds) - let _ = innerFile - try container.encode(size, forKey: .explicitSize) + try container.encode(size, forKey: .messageSize) + try container.encodeIfPresent(file, forKey: .messageFile) + try container.encodeIfPresent(mediaRect, forKey: .messageMediaRect) + try container.encodeIfPresent(mediaCornerRadius, forKey: .messageMediaCornerRadius) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) @@ -325,9 +343,15 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encode(self.mirrored, forKey: .mirrored) try container.encode(self.isExplicitlyStatic, forKey: .isExplicitlyStatic) + try container.encode(self.canCutOut, forKey: .canCutOut) + if let renderImage, let data = renderImage.pngData() { try container.encode(data, forKey: .renderImage) } + if let renderSubEntities = self.renderSubEntities { + let codableEntities: [CodableDrawingEntity] = renderSubEntities.compactMap { CodableDrawingEntity(entity: $0) } + try container.encode(codableEntities, forKey: .renderSubEntities) + } } public func duplicate(copy: Bool) -> DrawingEntity { @@ -341,6 +365,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { newEntity.rotation = self.rotation newEntity.mirrored = self.mirrored newEntity.isExplicitlyStatic = self.isExplicitlyStatic + newEntity.canCutOut = self.canCutOut return newEntity } @@ -372,6 +397,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { if self.isExplicitlyStatic != other.isExplicitlyStatic { return false } + if self.canCutOut != other.canCutOut { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index c39d2d363c..ba99243372 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -85,15 +85,17 @@ public final class DrawingMessageRenderer { private let context: AccountContext private let messages: [Message] private let isNight: Bool + private let isOverlay: Bool private let messagesContainerNode: ASDisplayNode private var avatarHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? - init(context: AccountContext, messages: [Message], isNight: Bool = false) { + init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false) { self.context = context self.messages = messages self.isNight = isNight + self.isOverlay = isOverlay self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -104,25 +106,8 @@ public final class DrawingMessageRenderer { self.addSubnode(self.messagesContainerNode) } - public func render(completion: @escaping (CGSize, UIImage?) -> Void) { - let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let defaultPresentationData = defaultPresentationData() - - var mockPresentationData = PresentationData( - strings: presentationData.strings, - theme: defaultPresentationTheme, - autoNightModeTriggered: false, - chatWallpaper: presentationData.chatWallpaper, - chatFontSize: defaultPresentationData.chatFontSize, - chatBubbleCorners: defaultPresentationData.chatBubbleCorners, - listsFontSize: defaultPresentationData.listsFontSize, - dateTimeFormat: presentationData.dateTimeFormat, - nameDisplayOrder: presentationData.nameDisplayOrder, - nameSortOrder: presentationData.nameSortOrder, - reduceMotion: false, - largeEmoji: true - ) - + public func render(presentationData: PresentationData, completion: @escaping (CGSize, UIImage?, CGRect?) -> Void) { + var mockPresentationData = presentationData if self.isNight { let darkTheme = defaultDarkColorPresentationTheme mockPresentationData = mockPresentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkTheme.chat.defaultWallpaper) @@ -132,8 +117,48 @@ public final class DrawingMessageRenderer { let size = self.updateMessagesLayout(layout: layout, presentationData: mockPresentationData) Queue.mainQueue().after(0.05, { + var mediaRect: CGRect? + if let messageNode = self.messageNodes?.first { + if self.isOverlay { + func hideNonOverlayViews(_ view: UIView) -> Bool { + var hasResult = false + for view in view.subviews { + if view.tag == 0xFACE { + hasResult = true + } else { + if hideNonOverlayViews(view) { + hasResult = true + } else { + view.isHidden = true + } + } + } + return hasResult + } + let _ = hideNonOverlayViews(messageNode.view) + } else if !self.isNight { + func findMediaView(_ view: UIView) -> UIView? { + for view in view.subviews { + if let _ = view.asyncdisplaykit_node as? UniversalVideoNode { + return view + } else { + if let result = findMediaView(view) { + return result + } + } + } + return nil + } + + if let mediaView = findMediaView(messageNode.view) { + var rect = mediaView.convert(mediaView.bounds, to: self.messagesContainerNode.view) + rect.origin.y = self.messagesContainerNode.frame.height - rect.maxY + mediaRect = rect + } + } + } self.generate(size: size) { image in - completion(size, image) + completion(size, image, mediaRect) } }) } @@ -259,11 +284,25 @@ public final class DrawingMessageRenderer { } } + public struct Result { + public struct MediaFrame { + public let rect: CGRect + public let cornerRadius: CGFloat + } + + public let size: CGSize + public let dayImage: UIImage + public let nightImage: UIImage + public let overlayImage: UIImage + public let mediaFrame: MediaFrame? + } + private let context: AccountContext private let messages: [Message] private let dayContainerNode: ContainerNode private let nightContainerNode: ContainerNode + private let overlayContainerNode: ContainerNode public init(context: AccountContext, messages: [Message]) { self.context = context @@ -271,27 +310,58 @@ public final class DrawingMessageRenderer { self.dayContainerNode = ContainerNode(context: context, messages: messages) self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true) } - public func render(completion: @escaping (CGSize, UIImage?, UIImage?) -> Void) { + public func render(completion: @escaping (Result) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let defaultPresentationData = defaultPresentationData() + + let mockPresentationData = PresentationData( + strings: presentationData.strings, + theme: defaultPresentationTheme, + autoNightModeTriggered: false, + chatWallpaper: presentationData.chatWallpaper, + chatFontSize: defaultPresentationData.chatFontSize, + chatBubbleCorners: defaultPresentationData.chatBubbleCorners, + listsFontSize: defaultPresentationData.listsFontSize, + dateTimeFormat: presentationData.dateTimeFormat, + nameDisplayOrder: presentationData.nameDisplayOrder, + nameSortOrder: presentationData.nameSortOrder, + reduceMotion: false, + largeEmoji: true + ) + var finalSize: CGSize = .zero var dayImage: UIImage? var nightImage: UIImage? + var overlayImage: UIImage? + var mediaRect: CGRect? let completeIfReady = { - if let dayImage, let nightImage { - completion(finalSize, dayImage, nightImage) + if let dayImage, let nightImage, let overlayImage { + var cornerRadius: CGFloat = defaultPresentationData.chatBubbleCorners.mainRadius + if let mediaRect, mediaRect.width == mediaRect.height, mediaRect.width == 240.0 { + cornerRadius = mediaRect.width / 2.0 + } else if let rect = mediaRect { + mediaRect = CGRect(x: rect.minX + 4.0, y: rect.minY, width: rect.width - 6.0, height: rect.height - 1.0) + } + completion(Result(size: finalSize, dayImage: dayImage, nightImage: nightImage, overlayImage: overlayImage, mediaFrame: mediaRect.flatMap { Result.MediaFrame(rect: $0, cornerRadius: cornerRadius) })) } } - self.dayContainerNode.render { size, image in + self.dayContainerNode.render(presentationData: mockPresentationData) { size, image, rect in finalSize = size dayImage = image + mediaRect = rect completeIfReady() } - self.nightContainerNode.render { size, image in - finalSize = size + self.nightContainerNode.render(presentationData: mockPresentationData) { size, image, _ in nightImage = image completeIfReady() } + self.overlayContainerNode.render(presentationData: mockPresentationData) { size, image, _ in + overlayImage = image + completeIfReady() + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index de2553c9d9..3815e2ac1b 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -74,6 +74,16 @@ public final class MediaEditor { public let top: UIColor public let bottom: UIColor + public init(colors: [UIColor]) { + if colors.count == 2 || colors.count == 1 { + self.top = colors.first! + self.bottom = colors.last! + } else { + self.top = .black + self.bottom = .black + } + } + public init(top: UIColor, bottom: UIColor) { self.top = top self.bottom = bottom @@ -110,7 +120,11 @@ public final class MediaEditor { private let clock = CMClockGetHostTimeClock() - private var player: AVPlayer? + private var player: AVPlayer? { + didSet { + + } + } private var playerAudioMix: AVMutableAudioMix? private var additionalPlayer: AVPlayer? @@ -146,11 +160,6 @@ public final class MediaEditor { private var textureSourceDisposable: Disposable? private let gradientColorsPromise = Promise() - private var gradientColorsValue: GradientColors? { - didSet { - self.gradientColorsPromise.set(.single(self.gradientColorsValue)) - } - } public var gradientColors: Signal { return self.gradientColorsPromise.get() } @@ -468,103 +477,53 @@ public final class MediaEditor { return } + let context = self.context + let clock = self.clock if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { print("error") } - - let context = self.context - let clock = self.clock - let textureSource: Signal<(UIImage?, UIImage?, AVPlayer?, AVPlayer?, GradientColors), NoError> - switch subject { - case let .image(image, _): - let colors = mediaEditorGetGradientColors(from: image) - textureSource = .single((image, nil, nil, nil, colors)) - case let .draft(draft): - if draft.isVideo { - textureSource = Signal { subscriber in - let url = URL(fileURLWithPath: draft.fullPath(engine: context.engine)) - let asset = AVURLAsset(url: url) - - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - if #available(iOS 15.0, *) { - player.sourceClock = clock - } else { - player.masterClock = clock - } - player.automaticallyWaitsToMinimizeStalling = false - - if let gradientColors = draft.values.gradientColors { - let colors = GradientColors(top: gradientColors.first!, bottom: gradientColors.last!) - subscriber.putNext((nil, nil, player, nil, colors)) - subscriber.putCompletion() - - return EmptyDisposable - } else { - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.appliesPreferredTrackTransform = true - imageGenerator.maximumSize = CGSize(width: 72, height: 128) - imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in - let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) - subscriber.putNext((nil, nil, player, nil, colors)) - subscriber.putCompletion() - } - return ActionDisposable { - imageGenerator.cancelAllCGImageGeneration() - } - } - } - } else { - guard let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) else { - return - } - let colors: GradientColors - if let gradientColors = draft.values.gradientColors { - colors = GradientColors(top: gradientColors.first!, bottom: gradientColors.last!) - } else { - colors = mediaEditorGetGradientColors(from: image) - } - textureSource = .single((image, nil, nil, nil, colors)) + + struct TextureSourceResult { + let image: UIImage? + let nightImage: UIImage? + let player: AVPlayer? + let playerIsReference: Bool + let gradientColors: GradientColors + + init(image: UIImage? = nil, nightImage: UIImage? = nil, player: AVPlayer? = nil, playerIsReference: Bool = false, gradientColors: GradientColors) { + self.image = image + self.nightImage = nightImage + self.player = player + self.playerIsReference = playerIsReference + self.gradientColors = gradientColors } - case let .video(path, transitionImage, mirror, _, _, _): - let _ = mirror - textureSource = Signal { subscriber in - let asset = AVURLAsset(url: URL(fileURLWithPath: path)) - let player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) - if #available(iOS 15.0, *) { - player.sourceClock = clock - } else { - player.masterClock = clock - } - player.automaticallyWaitsToMinimizeStalling = false - -// var additionalPlayer: AVPlayer? -// if let additionalPath { -// let additionalAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) -// additionalPlayer = AVPlayer(playerItem: AVPlayerItem(asset: additionalAsset)) -// if #available(iOS 15.0, *) { -// additionalPlayer?.sourceClock = clock -// } else { -// additionalPlayer?.masterClock = clock -// } -// additionalPlayer?.automaticallyWaitsToMinimizeStalling = false -// } - - if let transitionImage { - let colors = mediaEditorGetGradientColors(from: transitionImage) - //TODO pass mirror - subscriber.putNext((nil, nil, player, nil, colors)) + } + + func makePlayer(asset: AVAsset) -> AVPlayer { + let player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) + if #available(iOS 15.0, *) { + player.sourceClock = clock + } else { + player.masterClock = clock + } + player.automaticallyWaitsToMinimizeStalling = false + return player + } + + func textureSourceResult(for asset: AVAsset, gradientColors: GradientColors? = nil) -> Signal { + return Signal { subscriber in + let player = makePlayer(asset: asset) + if let gradientColors { + subscriber.putNext(TextureSourceResult(player: player, gradientColors: gradientColors)) subscriber.putCompletion() - return EmptyDisposable } else { let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true imageGenerator.maximumSize = CGSize(width: 72, height: 128) imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in - let colors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) - //TODO pass mirror - subscriber.putNext((nil, nil, player, nil, colors)) + let gradientColors: GradientColors = image.flatMap({ mediaEditorGetGradientColors(from: UIImage(cgImage: $0)) }) ?? GradientColors(top: .black, bottom: .black) + subscriber.putNext(TextureSourceResult(player: player, gradientColors: gradientColors)) subscriber.putCompletion() } return ActionDisposable { @@ -572,47 +531,55 @@ public final class MediaEditor { } } } + } + + let textureSource: Signal + switch subject { + case let .image(image, _): + textureSource = .single( + TextureSourceResult( + image: image, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + case let .draft(draft): + let gradientColors = draft.values.gradientColors.flatMap { GradientColors(colors: $0) } + let fullPath = draft.fullPath(engine: context.engine) + if draft.isVideo { + let url = URL(fileURLWithPath: fullPath) + let asset = AVURLAsset(url: url) + textureSource = textureSourceResult(for: asset, gradientColors: gradientColors) + } else { + guard let image = UIImage(contentsOfFile: fullPath) else { + return + } + textureSource = .single( + TextureSourceResult( + image: image, + gradientColors: gradientColors ?? mediaEditorGetGradientColors(from: image) + ) + ) + } + case let .video(path, _, mirror, _, _, _): + //TODO: pass mirror + let _ = mirror + let asset = AVURLAsset(url: URL(fileURLWithPath: path)) + textureSource = textureSourceResult(for: asset) case let .asset(asset): textureSource = Signal { subscriber in - if asset.mediaType == .video { - let options = PHImageRequestOptions() - options.deliveryMode = .fastFormat - options.isNetworkAccessAllowed = true - let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in - if let image { - if let info { - if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { - return - } - } - let colors = mediaEditorGetGradientColors(from: image) - PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in - if let asset { - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - player.automaticallyWaitsToMinimizeStalling = false - - #if targetEnvironment(simulator) - let additionalPlayerItem = AVPlayerItem(asset: asset) - let additionalPlayer = AVPlayer(playerItem: additionalPlayerItem) - additionalPlayer.automaticallyWaitsToMinimizeStalling = false - subscriber.putNext((nil, nil, player, additionalPlayer, colors)) - #else - subscriber.putNext((nil, nil, player, nil, colors)) - #endif - subscriber.putCompletion() - } - }) - } - }) - return ActionDisposable { - PHImageManager.default().cancelImageRequest(requestId) - } - } else { - let options = PHImageRequestOptions() - options.deliveryMode = .highQualityFormat - options.isNetworkAccessAllowed = true - let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 1920.0, height: 1920.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in + let isVideo = asset.mediaType == .video + + let targetSize = isVideo ? CGSize(width: 128.0, height: 128.0) : CGSize(width: 1920.0, height: 1920.0) + let options = PHImageRequestOptions() + options.deliveryMode = isVideo ? .fastFormat : .highQualityFormat + options.isNetworkAccessAllowed = true + + let requestId = PHImageManager.default().requestImage( + for: asset, + targetSize: targetSize, + contentMode: .aspectFit, + options: options, + resultHandler: { image, info in if let image { var degraded = false if let info { @@ -623,29 +590,63 @@ public final class MediaEditor { degraded = true } } - if !degraded { - let colors = mediaEditorGetGradientColors(from: image) - subscriber.putNext((image, nil, nil, nil, colors)) - subscriber.putCompletion() + if isVideo { + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in + if let asset { + let player = makePlayer(asset: asset) + subscriber.putNext( + TextureSourceResult( + player: player, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + subscriber.putCompletion() + } + }) + } else { + if !degraded { + subscriber.putNext( + TextureSourceResult( + image: image, + gradientColors: mediaEditorGetGradientColors(from: image) + ) + ) + subscriber.putCompletion() + } } } - }) - return ActionDisposable { - PHImageManager.default().cancelImageRequest(requestId) } + ) + return ActionDisposable { + PHImageManager.default().cancelImageRequest(requestId) } } case let .message(messageId): - textureSource = getChatWallpaperImage(context: self.context, messageId: messageId) - |> map { _, image, nightImage in - return (image, nightImage, nil, nil, GradientColors(top: .black, bottom: .black)) + textureSource = self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) + |> mapToSignal { message in + var player: AVPlayer? + if let message { + if let maybeFile = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let path = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: "mp4") { + let asset = AVURLAsset(url: URL(fileURLWithPath: path)) + player = makePlayer(asset: asset) + } + } + return getChatWallpaperImage(context: self.context, messageId: messageId) + |> map { _, image, nightImage in + return TextureSourceResult( + image: image, + nightImage: nightImage, + player: player, + playerIsReference: true, + gradientColors: GradientColors(top: .black, bottom: .black) + ) + } } } self.textureSourceDisposable = (textureSource - |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in + |> deliverOnMainQueue).start(next: { [weak self] textureSourceResult in if let self { - let (image, nightImage, player, additionalPlayer, colors) = sourceAndColors self.renderer.onNextRender = { [weak self] in self?.onFirstDisplay() } @@ -653,25 +654,22 @@ public final class MediaEditor { let textureSource = UniversalTextureSource(renderTarget: renderTarget) if case .message = self.self.subject { - if let image { - self.wallpapers = (image, nightImage ?? image) + if let image = textureSourceResult.image { + self.wallpapers = (image, textureSourceResult.nightImage ?? image) } } - self.player = player + self.player = textureSourceResult.player self.playerPromise.set(.single(player)) - - self.additionalPlayer = additionalPlayer - self.additionalPlayerPromise.set(.single(additionalPlayer)) - if let image { - if self.values.nightTheme, let nightImage { + if let image = textureSourceResult.image { + if self.values.nightTheme, let nightImage = textureSourceResult.nightImage { textureSource.setMainInput(.image(nightImage)) } else { textureSource.setMainInput(.image(image)) } } - if let player, let playerItem = player.currentItem { + if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference { textureSource.setMainInput(.video(playerItem)) } if let additionalPlayer, let playerItem = additionalPlayer.currentItem { @@ -679,13 +677,12 @@ public final class MediaEditor { } self.renderer.textureSource = textureSource - self.gradientColorsValue = colors - self.setGradientColors(colors.array) + self.setGradientColors(textureSourceResult.gradientColors) - if player == nil { + if let _ = textureSourceResult.player { self.updateRenderChain() // let _ = image - self.maybeGeneratePersonSegmentation(image) +// self.maybeGeneratePersonSegmentation(image) } if let _ = self.values.audioTrack { @@ -697,7 +694,7 @@ public final class MediaEditor { player.isMuted = self.values.videoIsMuted if let trimRange = self.values.videoTrimRange { player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) - additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) +// additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) } if let initialSeekPosition = self.initialSeekPosition { @@ -711,7 +708,7 @@ public final class MediaEditor { Queue.mainQueue().justDispatch { let startPlayback = { player.playImmediately(atRate: 1.0) - additionalPlayer?.playImmediately(atRate: 1.0) +// additionalPlayer?.playImmediately(atRate: 1.0) self.audioPlayer?.playImmediately(atRate: 1.0) self.onPlaybackAction(.play) self.volumeFadeIn = player.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) @@ -1616,9 +1613,10 @@ public final class MediaEditor { } } - public func setGradientColors(_ gradientColors: [UIColor]) { + public func setGradientColors(_ gradientColors: GradientColors) { + self.gradientColorsPromise.set(.single(gradientColors)) self.updateValues(mode: .skipRendering) { values in - return values.withUpdatedGradientColors(gradientColors: gradientColors) + return values.withUpdatedGradientColors(gradientColors: gradientColors.array) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 14718adb06..ab1c4569b5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -81,7 +81,14 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return [] case .message: if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { - return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)] + var entities: [MediaEditorComposerEntity] = [] + entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)) + if let renderSubEntities = entity.renderSubEntities { + for subEntity in renderSubEntities { + entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: subEntity, colorSpace: colorSpace)) + } + } + return entities } else { return [] } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index e9a09fad9f..c39dc2da94 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2277,7 +2277,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var effectiveSubject = subject if case let .draft(draft, _ ) = subject { for entity in draft.values.entities { - if case let .sticker(sticker) = entity, case let .message(ids, _, _) = sticker.content { + if case let .sticker(sticker) = entity, case let .message(ids, _, _, _, _) = sticker.content { effectiveSubject = .message(ids) break } @@ -2351,7 +2351,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { self.entitiesView.sendSubviewToBack(entityView) -// entityView.previewView = self.previewView entityView.updated = { [weak self, weak mediaEntity] in if let self, let mediaEntity { let rotationDelta = mediaEntity.rotation - initialRotation @@ -2445,10 +2444,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile + var messageFile: TelegramMediaFile? + if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: nil) { + messageFile = maybeFile + } let renderer = DrawingMessageRenderer(context: self.context, messages: messages) - renderer.render(completion: { size, dayImage, nightImage in + renderer.render(completion: { result in if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in if let stickerEntityView = entityView as? DrawingStickerEntityView, case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { return true @@ -2458,17 +2460,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) as? DrawingStickerEntityView { existingEntityView.isNightTheme = isNightTheme let messageEntity = existingEntityView.entity as! DrawingStickerEntity - messageEntity.renderImage = dayImage - messageEntity.secondaryRenderImage = nightImage + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage existingEntityView.update(animated: false) } else { - let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size)) - messageEntity.renderImage = dayImage - messageEntity.secondaryRenderImage = nightImage + let messageEntity = DrawingStickerEntity(content: .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)) + messageEntity.renderImage = result.dayImage + messageEntity.secondaryRenderImage = result.nightImage + messageEntity.overlayRenderImage = result.overlayImage messageEntity.referenceDrawingSize = storyDimensions messageEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) - let fraction = max(size.width, size.height) / 353.0 + let fraction = max(result.size.width, result.size.height) / 353.0 messageEntity.scale = min(6.0, 3.3 * fraction) if let entityView = self.entitiesView.add(messageEntity, announce: false) as? DrawingStickerEntityView { @@ -3365,7 +3369,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let completeWithImage: (UIImage) -> Void = { [weak self] image in let updatedImage = roundedImageWithTransparentCorners(image: image, cornerRadius: floor(image.size.width * 0.03))! - self?.interaction?.insertEntity(DrawingStickerEntity(content: .image(updatedImage, .rectangle)), scale: 2.5) + let entity = DrawingStickerEntity(content: .image(updatedImage, .rectangle)) + entity.canCutOut = false + + self?.interaction?.insertEntity(entity, scale: 2.5) } if let asset = result as? PHAsset { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 23cfac6d4f..b2bdedd8d5 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -804,9 +804,9 @@ private func extractAccountManagerState(records: AccountRecordsView MessageMediaEditingOptions case .Sticker: return [] case .Animated: - return [] + break case let .Video(_, _, flags, _): if flags.contains(.instantRoundVideo) { return []