diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 9f865a877b..6ad81cb574 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -274,6 +274,9 @@ final class PeekControllerNode: ViewControllerTracingNode { } func activateMenu() { + if self.content.menuItems().isEmpty { + return + } if case .press = self.content.menuActivation() { self.hapticFeedback.impact() } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift index 6be78f7964..8d6dde544e 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPack.swift @@ -3,44 +3,81 @@ import UIKit public class ImportStickerPack { public class Sticker: Equatable { + public enum Content { + case image(Data) + case animation(Data) + } + public static func == (lhs: ImportStickerPack.Sticker, rhs: ImportStickerPack.Sticker) -> Bool { return lhs.uuid == rhs.uuid } - let image: UIImage + let content: Content let emojis: [String] let uuid: UUID - init(image: UIImage, emojis: [String], uuid: UUID = UUID()) { - self.image = image + init(content: Content, emojis: [String], uuid: UUID = UUID()) { + self.content = content self.emojis = emojis self.uuid = uuid } + + var data: Data { + switch self.content { + case let .image(data): + return data + case let .animation(data): + return data + } + } } - public var identifier: String - public var name: String public let software: String - public var thumbnail: String? public let isAnimated: Bool - - public var stickers: [Sticker] + public let thumbnail: Sticker? + public let stickers: [Sticker] public init?(data: Data) { guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return nil } - - self.name = json["name"] as? String ?? "" - self.identifier = json["identifier"] as? String ?? "" self.software = json["software"] as? String ?? "" - self.isAnimated = json["isAnimated"] as? Bool ?? false + let isAnimated = json["isAnimated"] as? Bool ?? false + self.isAnimated = isAnimated + + func parseSticker(_ sticker: [String: Any]) -> Sticker? { + if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) { + var content: Sticker.Content? + switch mimeType.lowercased() { + case "image/png": + if !isAnimated { + content = .image(data) + } + case "application/x-tgsticker": + if isAnimated { + content = .animation(data) + } + default: + break + } + if let content = content { + return Sticker(content: content, emojis: sticker["emojis"] as? [String] ?? []) + } + } + return nil + } + + if let thumbnail = json["thumbnail"] as? [String: Any], let parsedSticker = parseSticker(thumbnail) { + self.thumbnail = parsedSticker + } else { + self.thumbnail = nil + } var stickers: [Sticker] = [] if let stickersArray = json["stickers"] as? [[String: Any]] { for sticker in stickersArray { - if let dataString = sticker["data"] as? String, let data = Data(base64Encoded: dataString), let image = UIImage(data: data) { - stickers.append(Sticker(image: image, emojis: sticker["emojis"] as? [String] ?? [])) + if let parsedSticker = parseSticker(sticker) { + stickers.append(parsedSticker) } } } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift index a90b7dbe85..68eee282ca 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -68,6 +68,9 @@ public final class ImportStickerPackController: ViewController, StandalonePresen self.controllerNode.cancel = { [weak self] in self?.dismiss() } + self.controllerNode.present = { [weak self] controller, arguments in + self?.present(controller, in: .window(.root), with: arguments) + } self.controllerNode.presentInGlobalOverlay = { [weak self] controller, arguments in self?.presentInGlobalOverlay(controller, with: arguments) } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 565fa76406..7bcb2bdd6b 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -68,6 +68,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll private var interaction: StickerPackPreviewInteraction! + var present: ((ViewController, Any?) -> Void)? var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? @@ -75,10 +76,13 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll let ready = Promise() private var didSetReady = false + private var pendingItems: [StickerPackPreviewGridEntry] = [] private var currentItems: [StickerPackPreviewGridEntry] = [] private var hapticFeedback: HapticFeedback? + private let disposable = MetaDisposable() + init(context: AccountContext) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -151,6 +155,10 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll } } + deinit { + self.disposable.dispose() + } + override func didLoad() { super.didLoad() @@ -160,31 +168,23 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll self.contentGridNode.view.addGestureRecognizer(PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(ASDisplayNode, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.contentGridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { -// var menuItems: [PeekControllerMenuItem] = [] -// if let stickerPack = strongSelf.stickerPack, case let .result(info, _, _) = stickerPack, info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { -// if strongSelf.sendSticker != nil { -// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, font: .bold, action: { node, rect in -// if let strongSelf = self { -// return strongSelf.sendSticker?(.standalone(media: item.file), node, rect) ?? false -// } else { -// return false -// } -// })) -// } -// menuItems.append(PeekControllerMenuItem(title: isStarred ? strongSelf.presentationData.strings.Stickers_RemoveFromFavorites : strongSelf.presentationData.strings.Stickers_AddToFavorites, color: isStarred ? .destructive : .accent, action: { _, _ in -// if let strongSelf = self { -// if isStarred { -// let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() -// } else { -// let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() -// } -// } -// return true -// })) -// menuItems.append(PeekControllerMenuItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { _, _ in return true })) -// } -// return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) - return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: []))) + var menuItems: [ContextMenuItem] = [] + if strongSelf.currentItems.count > 1 { + menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ImportStickerPack_RemoveFromImport, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in + f(.dismissWithoutContent) + + if let strongSelf = self { + var updatedItems = strongSelf.currentItems + updatedItems.removeAll(where: { $0.stickerItem.uuid == item.uuid }) + strongSelf.pendingItems = updatedItems + + if let (layout, navigationHeight) = strongSelf.containerLayout { + strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + }))) + } + return .single((itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems))) } } return nil @@ -304,21 +304,16 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll var itemCount = 0 var animateIn = false - if let stickerPack = self.stickerPack { - var updatedItems: [StickerPackPreviewGridEntry] = [] - for item in stickerPack.stickers { - updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item)) - } + if let _ = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count { + let previousItems = self.currentItems + self.currentItems = self.pendingItems - if self.currentItems.isEmpty && !updatedItems.isEmpty { - let entities = generateTextEntities(stickerPack.name, enabledTypes: [.mention]) - let font = Font.medium(20.0) - self.contentTitleNode.attributedText = stringWithAppliedEntities(stickerPack.name, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: font, linkFont: font, boldFont: font, italicFont: font, boldItalicFont: font, fixedFont: font, blockQuoteFont: font) - animateIn = true - itemCount = updatedItems.count - } - transaction = StickerPackPreviewGridTransaction(previousList: self.currentItems, list: updatedItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) - self.currentItems = updatedItems + let titleFont = Font.medium(20.0) + self.contentTitleNode.attributedText = stringWithAppliedEntities(self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count)), entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont) + animateIn = true + itemCount = self.currentItems.count + + transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) } let titleSize = self.contentTitleNode.updateLayout(CGSize(width: contentContainerFrame.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) @@ -421,7 +416,45 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll } @objc func installActionButtonPressed() { - + let controller = importStickerPackTitleController(sharedContext: self.context.sharedContext, account: self.context.account, title: self.presentationData.strings.ImportStickerPack_ChooseName, text: self.presentationData.strings.ImportStickerPack_ChooseNameDescription, placeholder: "", doneButtonTitle: nil, value: nil, maxLength: 128, apply: { [weak self] title in + if let strongSelf = self, let stickerPack = strongSelf.stickerPack, var title = title { + title = title.trimmingTrailingSpaces() + let shortName = title.replacingOccurrences(of: " ", with: "") + "_by_laktyushin" + var stickers: [ImportSticker] = [] + for item in strongSelf.currentItems { + var dimensions = PixelDimensions(width: 512, height: 512) + if case let .image(data) = item.stickerItem.content, let image = UIImage(data: data) { + dimensions = PixelDimensions(image.size) + } + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.stickerItem.data) + stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions)) + } + var thumbnailSticker: ImportSticker? + if let thumbnail = stickerPack.thumbnail { + var dimensions = PixelDimensions(width: 512, height: 512) + if case let .image(data) = thumbnail.content, let image = UIImage(data: data) { + dimensions = PixelDimensions(image.size) + } + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) + thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions) + } + + strongSelf.disposable.set(createStickerSet(account: strongSelf.context.account, title: title, shortName: shortName, stickers: stickers, thumbnail: thumbnailSticker, isAnimated: stickerPack.isAnimated).start(next: { [weak self] status in + if let strongSelf = self { + if case let .complete(pack, items) = status { + print("done!") + } + } + }, error: { error in + if let strongSelf = self { + + } + })) + } + }) + self.present?(controller, nil) } func animateIn() { @@ -461,13 +494,18 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll func updateStickerPack(_ stickerPack: ImportStickerPack) { self.stickerPack = stickerPack + var updatedItems: [StickerPackPreviewGridEntry] = [] + for item in stickerPack.stickers { + updatedItems.append(StickerPackPreviewGridEntry(index: updatedItems.count, stickerItem: item)) + } + self.pendingItems = updatedItems // self.interaction.playAnimatedStickers = stickerSettings.loopAnimatedStickers if let _ = self.containerLayout { self.dequeueUpdateStickerPack() } - self.installActionButtonNode.setTitle("Create Sticker Set", with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + self.installActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) // switch stickerPack { // case .none, .fetching: // self.installActionSeparatorNode.alpha = 0.0 diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift new file mode 100644 index 0000000000..95a7b1d9a2 --- /dev/null +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackTitleController.swift @@ -0,0 +1,468 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import AccountContext +import UrlEscaping + +private final class ImportStickerPackTitleInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + private let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + private let clearButton: HighlightableButtonNode + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + if self.textInputNode.isFirstResponder() { + self.clearButton.isHidden = newValue.isEmpty + } else { + self.clearButton.isHidden = true + } + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + private let maxLength: Int + + init(theme: PresentationTheme, placeholder: String, maxLength: Int, returnKeyType: UIReturnKeyType = .done) { + self.theme = theme + self.maxLength = maxLength + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .default + self.textInputNode.autocapitalizationType = .sentences + self.textInputNode.returnKeyType = returnKeyType + self.textInputNode.autocorrectionType = .default + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + self.clearButton = HighlightableButtonNode() + self.clearButton.imageNode.displaysAsynchronously = false + self.clearButton.imageNode.displayWithoutProcessing = true + self.clearButton.displaysAsynchronously = false + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + self.clearButton.isHidden = true + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + self.addSubnode(self.clearButton) + + self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + self.clearButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: theme.actionSheet.inputClearButtonColor), for: []) + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - 20.0, height: backgroundFrame.size.height))) + + if let image = self.clearButton.image(for: []) { + transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX - 8.0 - image.size.width, y: backgroundFrame.minY + floor((backgroundFrame.size.height - image.size.height) / 2.0)), size: image.size)) + } + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + self.clearButton.isHidden = !self.placeholderNode.isHidden + } + + func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { + self.clearButton.isHidden = (editableTextNode.textView.text ?? "").isEmpty + } + + func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { + self.clearButton.isHidden = true + } + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + if updatedText.count > maxLength { + self.textInputNode.layer.addShakeAnimation() + return false + } + if text == "\n" { + self.complete?() + return false + } + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - 20.0, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(33.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.placeholderNode.isHidden = false + self.clearButton.isHidden = true + + self.textInputNode.attributedText = nil + self.updateHeight?() + } +} + +private final class ImportStickerPackTitleAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + private let title: String + private let text: String + + private let titleNode: ASTextNode + private let textNode: ASTextNode + let inputFieldNode: ImportStickerPackTitleInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String, placeholder: String, value: String?, maxLength: Int) { + self.strings = strings + self.title = title + self.text = text + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 8 + + self.inputFieldNode = ImportStickerPackTitleInputFieldNode(theme: ptheme, placeholder: placeholder, maxLength: maxLength) + self.inputFieldNode.text = value ?? "" + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var value: String { + return self.inputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + let spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let inputFieldWidth = resultWidth + let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition) + let inputHeight = inputFieldHeight + transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom) + + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +func importStickerPackTitleController(sharedContext: SharedAccountContext, account: Account, title: String, text: String, placeholder: String, doneButtonTitle: String? = nil, value: String?, maxLength: Int, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = sharedContext.currentPresentationData.with { $0 } + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: doneButtonTitle ?? presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = ImportStickerPackTitleAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text, placeholder: placeholder, value: value, maxLength: maxLength) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + dismissImpl?(true) + + let previousValue = value ?? "" + let newValue = contentNode.value.trimmingCharacters(in: .whitespacesAndNewlines) + apply(previousValue != newValue || value == nil ? newValue : nil) + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = sharedContext.presentationData.start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] animated in + contentNode?.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift index bc6fd32bf2..c81ce71a9e 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPackPreviewGridItem.swift @@ -57,12 +57,10 @@ final class StickerPackPreviewGridItem: GridItem { private let textFont = Font.regular(20.0) final class StickerPackPreviewGridItemNode: GridItemNode { - private var currentState: (Account, ImportStickerPack.Sticker?)? + private var currentState: (Account, ImportStickerPack.Sticker?, CGSize)? private var isEmpty: Bool? -// private let imageNode: TransformImageNode private let imageNode: ASImageNode private var animationNode: AnimatedStickerNode? - private var placeholderNode: StickerShimmerEffectNode? private var theme: PresentationTheme? @@ -86,50 +84,16 @@ final class StickerPackPreviewGridItemNode: GridItemNode { override init() { self.imageNode = ASImageNode() -// self.imageNode = TransformImageNode() -// self.imageNode.isLayerBacked = !smartInvertColorsEnabled() - self.placeholderNode = StickerShimmerEffectNode() - self.placeholderNode?.isUserInteractionEnabled = false - + super.init() self.addSubnode(self.imageNode) - if let placeholderNode = self.placeholderNode { -// self.addSubnode(placeholderNode) - } - - var firstTime = true -// self.imageNode.imageUpdated = { [weak self] image in -// guard let strongSelf = self else { -// return -// } -// if image != nil { -// strongSelf.removePlaceholder(animated: !firstTime) -// } -// firstTime = false -// } } deinit { self.stickerFetchedDisposable.dispose() } - private func removePlaceholder(animated: Bool) { - if let placeholderNode = self.placeholderNode { - self.placeholderNode = nil - if !animated { - placeholderNode.removeFromSupernode() - } else { - placeholderNode.allowsGroupOpacity = true - placeholderNode.alpha = 0.0 - placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in - placeholderNode?.removeFromSupernode() - placeholderNode?.allowsGroupOpacity = false - }) - } - } - } - override func didLoad() { super.didLoad() @@ -141,47 +105,34 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.theme = theme if self.currentState == nil || self.currentState!.0 !== account || self.currentState!.1 !== stickerItem || self.isEmpty != isEmpty { + var dimensions = CGSize(width: 512.0, height: 512.0) if let stickerItem = stickerItem { - self.imageNode.image = stickerItem.image -// if stickerItem.file.isAnimatedSticker { -// let dimensions = stickerItem.file.dimensions ?? PixelDimensions(width: 512, height: 512) -// self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: account.postbox, file: stickerItem.file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))) -// -// if self.animationNode == nil { -// let animationNode = AnimatedStickerNode() -// self.animationNode = animationNode -// self.addSubnode(animationNode) -// animationNode.started = { [weak self] in -// self?.imageNode.isHidden = true -// self?.removePlaceholder(animated: false) -// } -// } -// let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) -// self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) -// self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true -// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) -// } else { -// if let animationNode = self.animationNode { -// animationNode.visibility = false -// self.animationNode = nil -// animationNode.removeFromSupernode() -// } -// self.imageNode.setSignal(chatMessageSticker(account: account, file: stickerItem.file, small: true)) -// self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: chatMessageStickerResource(file: stickerItem.file, small: true)).start()) -// } - } else { - if let placeholderNode = self.placeholderNode { - if isEmpty { - if !placeholderNode.alpha.isZero { - placeholderNode.alpha = 0.0 - placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + switch stickerItem.content { + case let .image(data): + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() } - } else { - placeholderNode.alpha = 1.0 - } + self.imageNode.isHidden = false + if let image = UIImage(data: data) { + self.imageNode.image = image + dimensions = image.size + } + case let .animation(data): + self.imageNode.isHidden = true + let animationNode = AnimatedStickerNode() + self.animationNode = animationNode + self.addSubnode(animationNode) + + let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) +// animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true } + } else { + dimensions = CGSize() } - self.currentState = (account, stickerItem) + self.currentState = (account, stickerItem, dimensions) self.setNeedsLayout() } self.isEmpty = isEmpty @@ -194,32 +145,13 @@ final class StickerPackPreviewGridItemNode: GridItemNode { let boundsSide = min(bounds.size.width - 14.0, bounds.size.height - 14.0) let boundingSize = CGSize(width: boundsSide, height: boundsSide) - if let placeholderNode = self.placeholderNode { - let placeholderFrame = CGRect(origin: CGPoint(x: floor((bounds.width - boundingSize.width) / 2.0), y: floor((bounds.height - boundingSize.height) / 2.0)), size: boundingSize) - placeholderNode.frame = bounds - -// if let theme = self.theme, let (_, stickerItem) = self.currentState, let item = stickerItem { -// placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size) -// } - } - - self.imageNode.frame = bounds -// if let (_, item) = self.currentState { -// if let item = item, let dimensions = item.file.dimensions?.cgSize { -// let imageSize = dimensions.aspectFitted(boundingSize) -// self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() -// self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) -// if let animationNode = self.animationNode { -// animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) -// animationNode.updateLayout(size: imageSize) -// } -// } -// } - } - - override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) { - if let placeholderNode = self.placeholderNode { - placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize) + if let (_, _, dimensions) = self.currentState { + let imageSize = dimensions.aspectFitted(boundingSize) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + if let animationNode = self.animationNode { + animationNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.updateLayout(size: imageSize) + } } } @@ -232,7 +164,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { func updatePreviewing(animated: Bool) { var isPreviewing = false - if let (_, maybeItem) = self.currentState, let interaction = self.interaction, let item = maybeItem { + if let (_, maybeItem, _) = self.currentState, let interaction = self.interaction, let item = maybeItem { isPreviewing = interaction.previewedItem === item } if self.currentIsPreviewing != isPreviewing { diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index 682002a352..86c976795b 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -68,15 +68,11 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController self.textNode = ASTextNode() self.imageNode = ASImageNode() self.imageNode.displaysAsynchronously = false - self.imageNode.image = item.image - + if case let .image(data) = item.content, let image = UIImage(data: data) { + self.imageNode.image = image + } self.textNode.attributedText = NSAttributedString(string: item.emojis.joined(separator: " "), font: Font.regular(32.0), textColor: .black) - -// for case let .Sticker(text, _, _) in item.file.attributes { -// self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(32.0), textColor: .black) -// break -// } - + // if item.file.isAnimatedSticker { // let animationNode = AnimatedStickerNode() // self.animationNode = animationNode @@ -92,10 +88,6 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController // self.animationNode = nil // } - - -// self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: false, fetched: true)) - super.init() self.isUserInteractionEnabled = false diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index 1c66ba5015..702201f77c 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -330,8 +330,13 @@ - (void)updateWithFetchResult:(TGMediaAssetFetchResult *)fetchResult { TGMediaAsset *currentAsset = ((TGMediaPickerGalleryItem *)_galleryController.currentItem).asset; - bool exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound); + bool exists; + if ([currentAsset isKindOfClass:[TGCameraCapturedVideo class]]) { + exists = [fetchResult indexOfAsset:((TGCameraCapturedVideo *)currentAsset).originalAsset] != NSNotFound; + } else { + exists = ([fetchResult indexOfAsset:currentAsset] != NSNotFound); + } if (!exists) { _galleryModel.dismiss(true, false);