mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various improvements
This commit is contained in:
parent
aeafae62df
commit
77df1cf45a
182 changed files with 6119 additions and 5449 deletions
|
|
@ -8478,11 +8478,13 @@ Sorry for the inconvenience.";
|
|||
"Conversation.SuggestedPhotoText" = "%@ suggests you to use this photo for your Telegram account.";
|
||||
"Conversation.SuggestedPhotoTextYou" = "You suggested %@ to use this photo for their Telegram account.";
|
||||
"Conversation.SuggestedPhotoView" = "View";
|
||||
"Conversation.SuggestedPhotoSuccess" = "You have updated your Telegram account photo.";
|
||||
|
||||
"Conversation.SuggestedVideoTitle" = "Suggested Video";
|
||||
"Conversation.SuggestedVideoText" = "%@ suggests you to use this video for your Telegram account.";
|
||||
"Conversation.SuggestedVideoTextYou" = "You suggested %@ to use this video for their Telegram account.";
|
||||
"Conversation.SuggestedVideoView" = "View";
|
||||
"Conversation.SuggestedVideoSuccess" = "You have updated your Telegram account video.";
|
||||
|
||||
"CacheEvictionMenu.CategoryExceptions_1" = "%@ Exception";
|
||||
"CacheEvictionMenu.CategoryExceptions_any" = "%@ Exceptions";
|
||||
|
|
@ -8493,9 +8495,6 @@ Sorry for the inconvenience.";
|
|||
"Notification.SuggestedProfilePhoto" = "Suggested Profile Photo";
|
||||
"Notification.SuggestedProfileVideo" = "Suggested Profile Video";
|
||||
|
||||
"PhotoEditor.SetProfilePhoto" = "Set Profile Photo";
|
||||
"PhotoEditor.SetProfileVideo" = "Set Profile Video";
|
||||
|
||||
"PhotoEditor.SetAsMyPhoto" = "Set as My Photo";
|
||||
"PhotoEditor.SetAsMyVideo" = "Set as My Video";
|
||||
|
||||
|
|
@ -8504,8 +8503,13 @@ Sorry for the inconvenience.";
|
|||
"Privacy.ProfilePhoto.SetPublicPhoto" = "Set Public Photo";
|
||||
"Privacy.ProfilePhoto.UpdatePublicPhoto" = "Update Public Photo";
|
||||
"Privacy.ProfilePhoto.RemovePublicPhoto" = "Remove Public Photo";
|
||||
"Privacy.ProfilePhoto.RemovePublicVideo" = "Remove Public Video";
|
||||
"Privacy.ProfilePhoto.PublicPhotoInfo" = "You can upload a public photo for those who are restricted from viewing your real profile photo.";
|
||||
"Privacy.ProfilePhoto.PublicPhotoSuccess" = "This photo is now set for those who are restricted from viewing your main photo.";
|
||||
"Privacy.ProfilePhoto.PublicVideoSuccess" = "This video is now set for those who are restricted from viewing your main photo.";
|
||||
|
||||
"WebApp.AddToAttachmentAllowMessages" = "Allow **%@** to send me messages";
|
||||
|
||||
"Common.Paste" = "Paste";
|
||||
|
||||
"PhotoEditor.SelectCoverFrameSuggestion" = "Choose a cover for profile video";
|
||||
|
|
|
|||
|
|
@ -706,6 +706,13 @@ public enum ChatListSearchFilter: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public enum InstalledStickerPacksControllerMode {
|
||||
case general
|
||||
case modal
|
||||
case masks
|
||||
case emoji
|
||||
}
|
||||
|
||||
public let defaultContactLabel: String = "_$!<Mobile>!$_"
|
||||
|
||||
public enum CreateGroupMode {
|
||||
|
|
@ -812,6 +819,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||
|
||||
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode) -> ViewController
|
||||
|
||||
func navigateToCurrentCall()
|
||||
var hasOngoingCall: ValuePromise<Bool> { get }
|
||||
var immediateHasOngoingCall: Bool { get }
|
||||
|
|
|
|||
|
|
@ -316,8 +316,8 @@ public final class AppLockContextImpl: AppLockContext {
|
|||
public var invalidAttempts: Signal<AccessChallengeAttempts?, NoError> {
|
||||
return self.currentState.get()
|
||||
|> map { state in
|
||||
return state.unlockAttemts.flatMap { unlockAttemts in
|
||||
return AccessChallengeAttempts(count: unlockAttemts.count, bootTimestamp: unlockAttemts.timestamp.bootTimestamp, uptime: unlockAttemts.timestamp.uptime)
|
||||
return state.unlockAttempts.flatMap { unlockAttempts in
|
||||
return AccessChallengeAttempts(count: unlockAttempts.count, bootTimestamp: unlockAttempts.timestamp.bootTimestamp, uptime: unlockAttempts.timestamp.uptime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -346,7 +346,7 @@ public final class AppLockContextImpl: AppLockContext {
|
|||
self.updateLockState { state in
|
||||
var state = state
|
||||
|
||||
state.unlockAttemts = nil
|
||||
state.unlockAttempts = nil
|
||||
|
||||
state.isManuallyLocked = false
|
||||
|
||||
|
|
@ -362,16 +362,16 @@ public final class AppLockContextImpl: AppLockContext {
|
|||
public func failedUnlockAttempt() {
|
||||
self.updateLockState { state in
|
||||
var state = state
|
||||
var unlockAttemts = state.unlockAttemts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0))
|
||||
var unlockAttempts = state.unlockAttempts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0))
|
||||
|
||||
unlockAttemts.count += 1
|
||||
unlockAttempts.count += 1
|
||||
|
||||
var bootTimestamp: Int32 = 0
|
||||
let uptime = getDeviceUptimeSeconds(&bootTimestamp)
|
||||
let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime)
|
||||
|
||||
unlockAttemts.timestamp = timestamp
|
||||
state.unlockAttemts = unlockAttemts
|
||||
unlockAttempts.timestamp = timestamp
|
||||
state.unlockAttempts = unlockAttempts
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ public struct UnlockAttempts: Codable, Equatable {
|
|||
public struct LockState: Codable, Equatable {
|
||||
public var isManuallyLocked: Bool
|
||||
public var autolockTimeout: Int32?
|
||||
public var unlockAttemts: UnlockAttempts?
|
||||
public var unlockAttempts: UnlockAttempts?
|
||||
public var applicationActivityTimestamp: MonotonicTimestamp?
|
||||
|
||||
public init(isManuallyLocked: Bool = false, autolockTimeout: Int32? = nil, unlockAttemts: UnlockAttempts? = nil, applicationActivityTimestamp: MonotonicTimestamp? = nil) {
|
||||
self.isManuallyLocked = isManuallyLocked
|
||||
self.autolockTimeout = autolockTimeout
|
||||
self.unlockAttemts = unlockAttemts
|
||||
self.unlockAttempts = unlockAttemts
|
||||
self.applicationActivityTimestamp = applicationActivityTimestamp
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -781,7 +781,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
|
|||
)), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
case let .addContact(phoneNumber, theme, strings):
|
||||
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
|
||||
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
|
||||
interaction.addContact(phoneNumber)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ swift_library(
|
|||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import StickerPeekUI
|
||||
import TelegramUIPreferences
|
||||
|
||||
public struct ChatInterfaceStickerSettings: Equatable {
|
||||
public let loopAnimatedStickers: Bool
|
||||
|
||||
public init(loopAnimatedStickers: Bool) {
|
||||
self.loopAnimatedStickers = loopAnimatedStickers
|
||||
}
|
||||
|
||||
public init(stickerSettings: StickerSettings) {
|
||||
self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool {
|
||||
return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatMediaInputGifMode: Equatable {
|
||||
case recent
|
||||
case trending
|
||||
case emojiSearch(String)
|
||||
}
|
||||
|
||||
public final class ChatMediaInputNodeInteraction {
|
||||
public let navigateToCollectionId: (ItemCollectionId) -> Void
|
||||
public let navigateBackToStickers: () -> Void
|
||||
public let setGifMode: (ChatMediaInputGifMode) -> Void
|
||||
public let openSettings: () -> Void
|
||||
public let openTrending: (ItemCollectionId?) -> Void
|
||||
public let dismissTrendingPacks: ([ItemCollectionId]) -> Void
|
||||
public let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void
|
||||
public let openPeerSpecificSettings: () -> Void
|
||||
public let dismissPeerSpecificSettings: () -> Void
|
||||
public let clearRecentlyUsedStickers: () -> Void
|
||||
|
||||
public var stickerSettings: ChatInterfaceStickerSettings?
|
||||
public var highlightedStickerItemCollectionId: ItemCollectionId?
|
||||
public var highlightedItemCollectionId: ItemCollectionId?
|
||||
public var highlightedGifMode: ChatMediaInputGifMode = .recent
|
||||
public var previewedStickerPackItem: StickerPreviewPeekItem?
|
||||
public var appearanceTransition: CGFloat = 1.0
|
||||
public var displayStickerPlaceholder = true
|
||||
public var displayStickerPackManageControls = true
|
||||
|
||||
public init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) {
|
||||
self.navigateToCollectionId = navigateToCollectionId
|
||||
self.navigateBackToStickers = navigateBackToStickers
|
||||
self.setGifMode = setGifMode
|
||||
self.openSettings = openSettings
|
||||
self.openTrending = openTrending
|
||||
self.dismissTrendingPacks = dismissTrendingPacks
|
||||
self.toggleSearch = toggleSearch
|
||||
self.openPeerSpecificSettings = openPeerSpecificSettings
|
||||
self.dismissPeerSpecificSettings = dismissPeerSpecificSettings
|
||||
self.clearRecentlyUsedStickers = clearRecentlyUsedStickers
|
||||
}
|
||||
}
|
||||
|
|
@ -181,6 +181,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||
public let topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
|
||||
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?
|
||||
public let externalBottomPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?
|
||||
public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void
|
||||
public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void
|
||||
|
|
@ -198,6 +199,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||
topPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
|
||||
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
||||
bottomPanel: AnyComponent<PagerComponentPanelEnvironment<TopPanelEnvironment>>?,
|
||||
externalBottomPanelContainer: PagerExternalTopPanelContainer?,
|
||||
panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?,
|
||||
isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void,
|
||||
isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void,
|
||||
|
|
@ -214,6 +216,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||
self.topPanel = topPanel
|
||||
self.externalTopPanelContainer = externalTopPanelContainer
|
||||
self.bottomPanel = bottomPanel
|
||||
self.externalBottomPanelContainer = externalBottomPanelContainer
|
||||
self.panelStateUpdated = panelStateUpdated
|
||||
self.isTopPanelExpandedUpdated = isTopPanelExpandedUpdated
|
||||
self.isTopPanelHiddenUpdated = isTopPanelHiddenUpdated
|
||||
|
|
@ -248,6 +251,9 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||
if lhs.bottomPanel != rhs.bottomPanel {
|
||||
return false
|
||||
}
|
||||
if lhs.externalBottomPanelContainer !== rhs.externalBottomPanelContainer {
|
||||
return false
|
||||
}
|
||||
if lhs.panelHideBehavior != rhs.panelHideBehavior {
|
||||
return false
|
||||
}
|
||||
|
|
@ -546,8 +552,13 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
|
|||
bottomPanelTransition = .immediate
|
||||
bottomPanelView = ComponentHostView<PagerComponentPanelEnvironment<TopPanelEnvironment>>()
|
||||
self.bottomPanelView = bottomPanelView
|
||||
self.addSubview(bottomPanelView)
|
||||
}
|
||||
|
||||
let bottomPanelSuperview = component.externalBottomPanelContainer ?? self
|
||||
if bottomPanelView.superview !== bottomPanelSuperview {
|
||||
bottomPanelSuperview.addSubview(bottomPanelView)
|
||||
}
|
||||
|
||||
let bottomPanelSize = bottomPanelView.update(
|
||||
transition: bottomPanelTransition,
|
||||
component: bottomPanel,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import PhoneNumberFormat
|
||||
import AccountContext
|
||||
|
||||
private let titleFont = Font.regular(17.0)
|
||||
|
||||
public class ContactsAddItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let phoneNumber: String
|
||||
|
|
@ -18,7 +20,8 @@ public class ContactsAddItem: ListViewItem {
|
|||
|
||||
public let header: ListViewItemHeader?
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, phoneNumber: String, header: ListViewItemHeader?, action: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.phoneNumber = phoneNumber
|
||||
|
|
@ -187,7 +190,7 @@ class ContactsAddItemNode: ListViewItemNode {
|
|||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let rightInset: CGFloat = 10.0 + params.rightInset
|
||||
|
||||
let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor)
|
||||
let titleAttributedString = NSAttributedString(string: item.strings.Contacts_AddPhoneNumber(formatPhoneNumber(context: item.context, number: item.phoneNumber)).string, font: titleFont, textColor: item.theme.list.itemAccentColor)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ private enum ContactListSearchEntry: Comparable, Identifiable {
|
|||
func item(context: AccountContext, presentationData: PresentationData, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, timeFormat: PresentationDateTimeFormat, addContact: ((String) -> Void)?, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?) -> ListViewItem {
|
||||
switch self {
|
||||
case let .addContact(theme, strings, phoneNumber):
|
||||
return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
|
||||
return ContactsAddItem(context: context, theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: {
|
||||
addContact?(phoneNumber)
|
||||
})
|
||||
case let .peer(_, theme, strings, peer, presence, group, enabled):
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ swift_library(
|
|||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -2274,10 +2274,10 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
|
||||
let swatch1Button = swatch1Button.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .pallete(false),
|
||||
type: .pallete(state.selectedColor == DrawingColor(color: .black)),
|
||||
color: DrawingColor(color: .black),
|
||||
action: {
|
||||
|
||||
state.updateColor(DrawingColor(color: .black))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
|
|
@ -2290,10 +2290,10 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
|
||||
let swatch2Button = swatch2Button.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .pallete(false),
|
||||
type: .pallete(state.selectedColor == DrawingColor(rgb: 0x0161fd)),
|
||||
color: DrawingColor(rgb: 0x0161fd),
|
||||
action: {
|
||||
|
||||
state.updateColor(DrawingColor(rgb: 0x0161fd))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
|
|
@ -2306,9 +2306,10 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
|
||||
let swatch3Button = swatch3Button.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .pallete(false),
|
||||
type: .pallete(state.selectedColor == DrawingColor(rgb: 0x32c759)),
|
||||
color: DrawingColor(rgb: 0x32c759),
|
||||
action: {
|
||||
state.updateColor(DrawingColor(rgb: 0x32c759))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
|
|
@ -2321,9 +2322,10 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
|
||||
let swatch4Button = swatch4Button.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .pallete(false),
|
||||
type: .pallete(state.selectedColor == DrawingColor(rgb: 0xffcc02)),
|
||||
color: DrawingColor(rgb: 0xffcc02),
|
||||
action: {
|
||||
state.updateColor(DrawingColor(rgb: 0xffcc02))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
|
|
@ -2336,9 +2338,10 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
|
||||
let swatch5Button = swatch5Button.update(
|
||||
component: ColorSwatchComponent(
|
||||
type: .pallete(false),
|
||||
type: .pallete(state.selectedColor == DrawingColor(rgb: 0xff3a30)),
|
||||
color: DrawingColor(rgb: 0xff3a30),
|
||||
action: {
|
||||
state.updateColor(DrawingColor(rgb: 0xff3a30))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
|
|
|
|||
|
|
@ -3,8 +3,20 @@ import UIKit
|
|||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingBubbleEntity: DrawingEntity {
|
||||
public enum DrawType {
|
||||
final class DrawingBubbleEntity: DrawingEntity, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case drawType
|
||||
case color
|
||||
case lineWidth
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
case size
|
||||
case rotation
|
||||
case tailPosition
|
||||
}
|
||||
|
||||
public enum DrawType: Codable {
|
||||
case fill
|
||||
case stroke
|
||||
}
|
||||
|
|
@ -22,6 +34,10 @@ final class DrawingBubbleEntity: DrawingEntity {
|
|||
var rotation: CGFloat
|
||||
var tailPosition: CGPoint
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
init(drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
|
@ -37,10 +53,33 @@ final class DrawingBubbleEntity: DrawingEntity {
|
|||
self.tailPosition = CGPoint(x: 0.16, y: 0.18)
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
self.isAnimated = false
|
||||
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||
self.tailPosition = try container.decode(CGPoint.self, forKey: .tailPosition)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.uuid, forKey: .uuid)
|
||||
try container.encode(self.drawType, forKey: .drawType)
|
||||
try container.encode(self.color, forKey: .color)
|
||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
try container.encode(self.size, forKey: .size)
|
||||
try container.encode(self.rotation, forKey: .rotation)
|
||||
try container.encode(self.tailPosition, forKey: .tailPosition)
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
|
|
@ -138,9 +177,6 @@ final class DrawingBubbleEntityView: DrawingEntityView {
|
|||
return
|
||||
}
|
||||
|
||||
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
// selectionView.scale = scale
|
||||
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.bubbleEntity.rotation)
|
||||
selectionView.setNeedsLayout()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,102 @@ protocol DrawingEntity: AnyObject {
|
|||
func makeView(context: AccountContext) -> DrawingEntityView
|
||||
}
|
||||
|
||||
enum CodableDrawingEntity {
|
||||
case sticker(DrawingStickerEntity)
|
||||
case text(DrawingTextEntity)
|
||||
case simpleShape(DrawingSimpleShapeEntity)
|
||||
case bubble(DrawingBubbleEntity)
|
||||
case vector(DrawingVectorEntity)
|
||||
|
||||
init?(entity: DrawingEntity) {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
self = .sticker(entity)
|
||||
} else if let entity = entity as? DrawingTextEntity {
|
||||
self = .text(entity)
|
||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
self = .simpleShape(entity)
|
||||
} else if let entity = entity as? DrawingBubbleEntity {
|
||||
self = .bubble(entity)
|
||||
} else if let entity = entity as? DrawingVectorEntity {
|
||||
self = .vector(entity)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var entity: DrawingEntity {
|
||||
switch self {
|
||||
case let .sticker(entity):
|
||||
return entity
|
||||
case let .text(entity):
|
||||
return entity
|
||||
case let .simpleShape(entity):
|
||||
return entity
|
||||
case let .bubble(entity):
|
||||
return entity
|
||||
case let .vector(entity):
|
||||
return entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CodableDrawingEntity: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case entity
|
||||
}
|
||||
|
||||
private enum EntityType: Int, Codable {
|
||||
case sticker
|
||||
case text
|
||||
case simpleShape
|
||||
case bubble
|
||||
case vector
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(EntityType.self, forKey: .type)
|
||||
switch type {
|
||||
case .sticker:
|
||||
self = .sticker(try container.decode(DrawingStickerEntity.self, forKey: .entity))
|
||||
case .text:
|
||||
self = .text(try container.decode(DrawingTextEntity.self, forKey: .entity))
|
||||
case .simpleShape:
|
||||
self = .simpleShape(try container.decode(DrawingSimpleShapeEntity.self, forKey: .entity))
|
||||
case .bubble:
|
||||
self = .bubble(try container.decode(DrawingBubbleEntity.self, forKey: .entity))
|
||||
case .vector:
|
||||
self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity))
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .sticker(payload):
|
||||
try container.encode(EntityType.sticker, forKey: .type)
|
||||
try container.encode(payload, forKey: .entity)
|
||||
case let .text(payload):
|
||||
try container.encode(EntityType.text, forKey: .type)
|
||||
try container.encode(payload, forKey: .entity)
|
||||
case let .simpleShape(payload):
|
||||
try container.encode(EntityType.simpleShape, forKey: .type)
|
||||
try container.encode(payload, forKey: .entity)
|
||||
case let .bubble(payload):
|
||||
try container.encode(EntityType.bubble, forKey: .type)
|
||||
try container.encode(payload, forKey: .entity)
|
||||
case let .vector(payload):
|
||||
try container.encode(EntityType.vector, forKey: .type)
|
||||
try container.encode(payload, forKey: .entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private let context: AccountContext
|
||||
private let size: CGSize
|
||||
|
||||
|
||||
weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
|
|
@ -30,16 +122,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
var selectionChanged: (DrawingEntity?) -> Void = { _ in }
|
||||
var requestedMenuForEntityView: (DrawingEntityView, Bool) -> Void = { _, _ in }
|
||||
|
||||
init(context: AccountContext, size: CGSize, entities: [DrawingEntity] = []) {
|
||||
public init(context: AccountContext, size: CGSize) {
|
||||
self.context = context
|
||||
self.size = size
|
||||
|
||||
super.init(frame: CGRect(origin: .zero, size: size))
|
||||
|
||||
for entity in entities {
|
||||
self.add(entity)
|
||||
}
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecognizer)
|
||||
self.tapGestureRecognizer = tapGestureRecognizer
|
||||
|
|
@ -48,7 +136,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
var entities: [DrawingEntity] {
|
||||
var entities: [DrawingEntity] = []
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
|
|
@ -57,6 +145,26 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
return entities
|
||||
}
|
||||
|
||||
public func setup(withEntitiesData entitiesData: Data!) {
|
||||
self.clear()
|
||||
|
||||
if let entitiesData = entitiesData, let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: entitiesData) {
|
||||
let entities = codableEntities.map { $0.entity }
|
||||
for entity in entities {
|
||||
self.add(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entitiesData: Data? {
|
||||
let entities = self.entities.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
if let data = try? JSONEncoder().encode(entities) {
|
||||
return data
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func startPosition(relativeTo entity: DrawingEntity?) -> CGPoint {
|
||||
let offsetLength = round(self.size.width * 0.1)
|
||||
let offset = CGPoint(x: offsetLength, y: offsetLength)
|
||||
|
|
@ -118,7 +226,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
sticker.position = center
|
||||
if setup {
|
||||
sticker.referenceDrawingSize = self.size
|
||||
sticker.scale = 1.0
|
||||
if !sticker.file.isVideoSticker && sticker.file.mimeType.hasSuffix("webm") {
|
||||
sticker.scale = 4.0
|
||||
} else {
|
||||
sticker.scale = 1.0
|
||||
}
|
||||
}
|
||||
} else if let bubble = entity as? DrawingBubbleEntity {
|
||||
bubble.position = center
|
||||
|
|
@ -171,11 +283,15 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
}
|
||||
|
||||
func removeAll() {
|
||||
self.clear()
|
||||
self.selectionChanged(nil)
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
|
||||
private func clear() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
self.selectionChanged(nil)
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
|
||||
func bringToFront(uuid: UUID) {
|
||||
|
|
@ -222,7 +338,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
view.updateVisibility(visibility)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func handleTap(_ gestureRecognzier: UITapGestureRecognizer) {
|
||||
let location = gestureRecognzier.location(in: self)
|
||||
|
||||
|
|
@ -256,7 +372,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let entity = entity, let entityView = self.getView(for: entity.uuid) {
|
||||
self.selectedEntityView = entityView
|
||||
|
||||
|
|
@ -298,7 +414,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
public func clearSelection() {
|
||||
self.selectEntity(nil)
|
||||
}
|
||||
|
|
@ -382,7 +498,7 @@ public class DrawingEntityView: UIView {
|
|||
return
|
||||
}
|
||||
self.pushIdentityTransformForMeasurement()
|
||||
|
||||
|
||||
selectionView.transform = .identity
|
||||
let bounds = self.selectionBounds
|
||||
let center = bounds.center
|
||||
|
|
|
|||
|
|
@ -969,7 +969,7 @@ struct Polyline {
|
|||
self.azimuth = azimuth
|
||||
self.touchPoint = touchPoint
|
||||
}
|
||||
|
||||
|
||||
init(touchPoint: TouchPath.Point) {
|
||||
self.location = touchPoint.event.location
|
||||
self.force = touchPoint.event.force
|
||||
|
|
@ -989,6 +989,17 @@ struct Polyline {
|
|||
touchPoint: self.touchPoint
|
||||
)
|
||||
}
|
||||
|
||||
func withLocation(_ point: CGPoint) -> Polyline.Point {
|
||||
return Point(
|
||||
location: point,
|
||||
force: self.force,
|
||||
altitudeAngle: self.altitudeAngle,
|
||||
azimuth: self.azimuth,
|
||||
velocity: self.velocity,
|
||||
touchPoint: self.touchPoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public internal(set) var isComplete: Bool
|
||||
|
|
@ -1023,9 +1034,8 @@ struct Polyline {
|
|||
}
|
||||
|
||||
init(points: [Point]) {
|
||||
assert(!points.isEmpty)
|
||||
self.isComplete = true
|
||||
self.touchIdentifier = points.first!.event.touchIdentifier
|
||||
self.touchIdentifier = points.first?.event.touchIdentifier ?? ""
|
||||
self.points = points
|
||||
}
|
||||
|
||||
|
|
@ -1063,7 +1073,6 @@ struct Polyline {
|
|||
}
|
||||
}
|
||||
|
||||
// Remove points from the end of the list toward the beginning
|
||||
for index in indexesToRemove.reversed() {
|
||||
guard index < points.count else {
|
||||
print("Error: unknown polyline index \(index)")
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import Foundation
|
|||
import UIKit
|
||||
import QuartzCore
|
||||
import MetalKit
|
||||
import Display
|
||||
import AppBundle
|
||||
|
||||
public final class DrawingMetalView: MTKView {
|
||||
final class DrawingMetalView: MTKView {
|
||||
private let size: CGSize
|
||||
|
||||
private let commandQueue: MTLCommandQueue
|
||||
|
|
@ -20,7 +21,7 @@ public final class DrawingMetalView: MTKView {
|
|||
private var markerBrush: Brush?
|
||||
private var pencilBrush: Brush?
|
||||
|
||||
public init?(size: CGSize) {
|
||||
init?(size: CGSize) {
|
||||
var size = size
|
||||
if Int(size.width) % 16 != 0 {
|
||||
size.width = round(size.width / 16.0) * 16.0
|
||||
|
|
@ -74,12 +75,22 @@ public final class DrawingMetalView: MTKView {
|
|||
}
|
||||
|
||||
func drawInContext(_ cgContext: CGContext) {
|
||||
guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
|
||||
// guard let texture = self.drawable?.texture, let ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: CGColorSpaceCreateDeviceRGB()])?.oriented(forExifOrientation: 1) else {
|
||||
// return
|
||||
// }
|
||||
// let context = CIContext(cgContext: cgContext)
|
||||
// let rect = CGRect(origin: .zero, size: ciImage.extent.size)
|
||||
// context.draw(ciImage, in: rect, from: rect)
|
||||
guard let texture = self.drawable?.texture, let image = texture.createCGImage() else {
|
||||
return
|
||||
}
|
||||
let context = CIContext(cgContext: cgContext)
|
||||
let rect = CGRect(origin: .zero, size: ciImage.extent.size)
|
||||
context.draw(ciImage, in: rect, from: rect)
|
||||
let rect = CGRect(origin: .zero, size: CGSize(width: image.width, height: image.height))
|
||||
cgContext.saveGState()
|
||||
cgContext.translateBy(x: rect.midX, y: rect.midY)
|
||||
cgContext.scaleBy(x: 1.0, y: -1.0)
|
||||
cgContext.translateBy(x: -rect.midX, y: -rect.midY)
|
||||
cgContext.draw(image, in: rect)
|
||||
cgContext.restoreGState()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
|
|
@ -116,7 +127,7 @@ public final class DrawingMetalView: MTKView {
|
|||
self.penBrush = Brush(texture: nil, target: self, rotation: .ahead)
|
||||
|
||||
if let url = getAppBundle().url(forResource: "marker", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(0.0))
|
||||
self.markerBrush = Brush(texture: self.makeTexture(with: data), target: self, rotation: .fixed(-0.55))
|
||||
}
|
||||
|
||||
if let url = getAppBundle().url(forResource: "pencil", withExtension: "png"), let data = try? Data(contentsOf: url) {
|
||||
|
|
@ -124,10 +135,11 @@ public final class DrawingMetalView: MTKView {
|
|||
}
|
||||
}
|
||||
|
||||
override public func draw(_ rect: CGRect) {
|
||||
var clearOnce = false
|
||||
override func draw(_ rect: CGRect) {
|
||||
super.draw(rect)
|
||||
|
||||
guard let drawable = self.drawable, let texture = drawable.texture else {
|
||||
guard let drawable = self.drawable, let texture = drawable.texture?.texture else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +150,10 @@ public final class DrawingMetalView: MTKView {
|
|||
attachment?.loadAction = .clear
|
||||
attachment?.storeAction = .store
|
||||
|
||||
guard let _ = attachment?.texture else {
|
||||
return
|
||||
}
|
||||
|
||||
let commandBuffer = self.commandQueue.makeCommandBuffer()
|
||||
let commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
|
||||
|
||||
|
|
@ -160,10 +176,11 @@ public final class DrawingMetalView: MTKView {
|
|||
return
|
||||
}
|
||||
|
||||
self.clearOnce = true
|
||||
drawable.updateBuffer(with: self.size)
|
||||
drawable.clear()
|
||||
|
||||
drawable.commit()
|
||||
drawable.commit(wait: true)
|
||||
}
|
||||
|
||||
enum BrushType {
|
||||
|
|
@ -182,10 +199,21 @@ public final class DrawingMetalView: MTKView {
|
|||
self.pencilBrush?.updated(point, color: color, state: state, size: size)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(_ points: [CGPoint], brush: BrushType, color: DrawingColor, size: CGFloat) {
|
||||
switch brush {
|
||||
case .pen:
|
||||
self.penBrush?.setup(points, color: color, size: size)
|
||||
case .marker:
|
||||
self.markerBrush?.setup(points, color: color, size: size)
|
||||
case .pencil:
|
||||
self.pencilBrush?.setup(points, color: color, size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class Drawable {
|
||||
public private(set) var texture: MTLTexture?
|
||||
public private(set) var texture: Texture?
|
||||
|
||||
internal var pixelFormat: MTLPixelFormat = .bgra8Unorm
|
||||
internal var size: CGSize
|
||||
|
|
@ -199,12 +227,12 @@ private class Drawable {
|
|||
self.size = size
|
||||
self.pixelFormat = pixelFormat
|
||||
self.device = device
|
||||
self.texture = self.makeColorTexture(DrawingColor.clear)
|
||||
self.texture = self.makeTexture()
|
||||
self.commandQueue = device?.makeCommandQueue()
|
||||
|
||||
self.renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
let attachment = self.renderPassDescriptor?.colorAttachments[0]
|
||||
attachment?.texture = self.texture
|
||||
attachment?.texture = self.texture?.texture
|
||||
attachment?.loadAction = .load
|
||||
attachment?.storeAction = .store
|
||||
|
||||
|
|
@ -212,9 +240,8 @@ private class Drawable {
|
|||
}
|
||||
|
||||
func clear() {
|
||||
self.texture = self.makeColorTexture(DrawingColor.clear)
|
||||
self.renderPassDescriptor?.colorAttachments[0].texture = self.texture
|
||||
self.commit()
|
||||
self.texture?.clear()
|
||||
self.commit(wait: true)
|
||||
}
|
||||
|
||||
internal func updateBuffer(with size: CGSize) {
|
||||
|
|
@ -226,49 +253,40 @@ private class Drawable {
|
|||
|
||||
internal func prepareForDraw() {
|
||||
if self.commandBuffer == nil {
|
||||
self.commandBuffer = commandQueue?.makeCommandBuffer()
|
||||
self.commandBuffer = self.commandQueue?.makeCommandBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
internal func makeCommandEncoder() -> MTLRenderCommandEncoder? {
|
||||
guard let commandBuffer = self.commandBuffer, let rpd = renderPassDescriptor else {
|
||||
guard let commandBuffer = self.commandBuffer, let renderPassDescriptor = self.renderPassDescriptor else {
|
||||
return nil
|
||||
}
|
||||
return commandBuffer.makeRenderCommandEncoder(descriptor: rpd)
|
||||
return commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
|
||||
}
|
||||
|
||||
internal func commit() {
|
||||
internal func commit(wait: Bool = false) {
|
||||
self.commandBuffer?.commit()
|
||||
if wait {
|
||||
self.commandBuffer?.waitUntilCompleted()
|
||||
}
|
||||
self.commandBuffer = nil
|
||||
}
|
||||
|
||||
internal func makeColorTexture(_ color: DrawingColor) -> MTLTexture? {
|
||||
guard self.size.width * self.size.height > 0 else {
|
||||
internal func makeTexture() -> Texture? {
|
||||
guard self.size.width * self.size.height > 0, let device = self.device else {
|
||||
return nil
|
||||
}
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
pixelFormat: self.pixelFormat,
|
||||
width: Int(self.size.width),
|
||||
height: Int(self.size.height),
|
||||
mipmapped: false
|
||||
)
|
||||
textureDescriptor.usage = [.renderTarget, .shaderRead]
|
||||
guard let texture = device?.makeTexture(descriptor: textureDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
let region = MTLRegion(
|
||||
origin: MTLOrigin(x: 0, y: 0, z: 0),
|
||||
size: MTLSize(width: texture.width, height: texture.height, depth: 1)
|
||||
)
|
||||
let bytesPerRow = 4 * texture.width
|
||||
let data = Data(capacity: Int(bytesPerRow * texture.height))
|
||||
if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
|
||||
texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow)
|
||||
}
|
||||
return texture
|
||||
return Texture(device: device, width: Int(self.size.width), height: Int(self.size.height))
|
||||
}
|
||||
}
|
||||
|
||||
private func alignUp(size: Int, align: Int) -> Int {
|
||||
precondition(((align - 1) & align) == 0, "Align must be a power of two")
|
||||
|
||||
let alignmentMask = align - 1
|
||||
return (size + alignmentMask) & ~alignmentMask
|
||||
}
|
||||
|
||||
private class Brush {
|
||||
private(set) var texture: MTLTexture?
|
||||
private(set) var pipelineState: MTLRenderPipelineState!
|
||||
|
|
@ -356,7 +374,7 @@ private class Brush {
|
|||
switch state {
|
||||
case .began:
|
||||
self.bezier.begin(with: point)
|
||||
self.pushPoint(point, color: color, size: size, isEnd: false)
|
||||
let _ = self.pushPoint(point, color: color, size: size, isEnd: false)
|
||||
case .changed:
|
||||
if self.bezier.points.count > 0 && point != lastRenderedPoint {
|
||||
self.pushPoint(point, color: color, size: size, isEnd: false)
|
||||
|
|
@ -370,6 +388,45 @@ private class Brush {
|
|||
}
|
||||
}
|
||||
|
||||
func setup(_ inputPoints: [CGPoint], color: DrawingColor, size: CGFloat) {
|
||||
guard inputPoints.count >= 2 else {
|
||||
return
|
||||
}
|
||||
var pointStep: CGFloat
|
||||
if case .random = self.rotation {
|
||||
pointStep = size * 0.1
|
||||
} else {
|
||||
pointStep = 2.0
|
||||
}
|
||||
|
||||
var lines: [Line] = []
|
||||
|
||||
var previousPoint = inputPoints[0]
|
||||
|
||||
var points: [CGPoint] = []
|
||||
self.bezier.begin(with: inputPoints.first!)
|
||||
for point in inputPoints {
|
||||
let smoothPoints = self.bezier.pushPoint(point)
|
||||
points.append(contentsOf: smoothPoints)
|
||||
}
|
||||
self.bezier.finish()
|
||||
|
||||
for i in 1 ..< points.count {
|
||||
let p = points[i]
|
||||
if (i == points.count - 1) || pointStep <= 1 || (pointStep > 1 && previousPoint.distance(to: p) >= pointStep) {
|
||||
let line = Line(start: previousPoint, end: p, pointSize: size, pointStep: pointStep)
|
||||
lines.append(line)
|
||||
previousPoint = p
|
||||
}
|
||||
}
|
||||
|
||||
if let drawable = self.target?.drawable {
|
||||
let stroke = Stroke(color: color, lines: lines, target: drawable)
|
||||
self.render(stroke: stroke, in: drawable)
|
||||
drawable.commit(wait: true)
|
||||
}
|
||||
}
|
||||
|
||||
private var lastRenderedPoint: CGPoint?
|
||||
func pushPoint(_ point: CGPoint, color: DrawingColor, size: CGFloat, isEnd: Bool) {
|
||||
var pointStep: CGFloat
|
||||
|
|
@ -396,9 +453,7 @@ private class Brush {
|
|||
|
||||
if let drawable = self.target?.drawable {
|
||||
let stroke = Stroke(color: color, lines: lines, target: drawable)
|
||||
|
||||
self.render(stroke: stroke, in: drawable)
|
||||
|
||||
drawable.commit()
|
||||
}
|
||||
}
|
||||
|
|
@ -489,8 +544,7 @@ class BezierGenerator {
|
|||
return []
|
||||
}
|
||||
step += 1
|
||||
let result = genericPathPoints()
|
||||
return result
|
||||
return self.generateSmoothPathPoints()
|
||||
}
|
||||
|
||||
func finish() {
|
||||
|
|
@ -501,7 +555,7 @@ class BezierGenerator {
|
|||
var points: [CGPoint] = []
|
||||
|
||||
private var step = 0
|
||||
private func genericPathPoints() -> [CGPoint] {
|
||||
private func generateSmoothPathPoints() -> [CGPoint] {
|
||||
var begin: CGPoint
|
||||
var control: CGPoint
|
||||
let end = CGPoint.middle(p1: points[step], p2: points[step + 1])
|
||||
|
|
@ -552,3 +606,149 @@ private struct Line {
|
|||
return self.end.angle(to: self.start)
|
||||
}
|
||||
}
|
||||
|
||||
final class Texture {
|
||||
#if !targetEnvironment(simulator)
|
||||
let buffer: MTLBuffer
|
||||
#endif
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
let bytesPerRow: Int
|
||||
let texture: MTLTexture
|
||||
|
||||
init?(device: MTLDevice, width: Int, height: Int) {
|
||||
let bytesPerPixel = 4
|
||||
let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm)
|
||||
let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = bytesPerRow
|
||||
|
||||
if #available(iOS 12.0, *) {
|
||||
#if targetEnvironment(simulator)
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.usage = [.renderTarget, .shaderRead]
|
||||
textureDescriptor.storageMode = .shared
|
||||
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else {
|
||||
return nil
|
||||
}
|
||||
self.buffer = buffer
|
||||
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.usage = [.renderTarget]
|
||||
textureDescriptor.storageMode = buffer.storageMode
|
||||
|
||||
guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
self.texture = texture
|
||||
} else {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.usage = [.renderTarget, .shaderRead]
|
||||
textureDescriptor.storageMode = .shared
|
||||
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.texture = texture
|
||||
}
|
||||
self.clear()
|
||||
|
||||
// let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
|
||||
// pixelFormat: self.pixelFormat,
|
||||
// width: Int(self.size.width),
|
||||
// height: Int(self.size.height),
|
||||
// mipmapped: false
|
||||
// )
|
||||
// textureDescriptor.usage = [.renderTarget, .shaderRead]
|
||||
// guard let texture = device?.makeTexture(descriptor: textureDescriptor) else {
|
||||
// return nil
|
||||
// }
|
||||
// let region = MTLRegion(
|
||||
// origin: MTLOrigin(x: 0, y: 0, z: 0),
|
||||
// size: MTLSize(width: texture.width, height: texture.height, depth: 1)
|
||||
// )
|
||||
// let bytesPerRow = 4 * texture.width
|
||||
// let data = Data(capacity: Int(bytesPerRow * texture.height))
|
||||
// if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
|
||||
// texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: bytesPerRow)
|
||||
// }
|
||||
// return texture
|
||||
}
|
||||
|
||||
func clear() {
|
||||
let region = MTLRegion(
|
||||
origin: MTLOrigin(x: 0, y: 0, z: 0),
|
||||
size: MTLSize(width: self.width, height: self.height, depth: 1)
|
||||
)
|
||||
let data = Data(capacity: Int(self.bytesPerRow * self.height))
|
||||
if let bytes = data.withUnsafeBytes({ $0.baseAddress }) {
|
||||
self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: self.bytesPerRow)
|
||||
}
|
||||
}
|
||||
|
||||
func createCGImage() -> CGImage? {
|
||||
#if targetEnvironment(simulator)
|
||||
guard let data = NSMutableData(capacity: self.bytesPerRow * self.height) else {
|
||||
return nil
|
||||
}
|
||||
data.length = self.bytesPerRow * self.height
|
||||
self.texture.getBytes(data.mutableBytes, bytesPerRow: self.bytesPerRow, bytesPerImage: self.bytesPerRow * self.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.width, height: self.height, depth: 1)), mipmapLevel: 0, slice: 0)
|
||||
|
||||
guard let dataProvider = CGDataProvider(data: data as CFData) else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
let content = self.content
|
||||
let pool = self.pool
|
||||
guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.buffer.contents(), count: self.buffer.length, deallocator: .custom { [weak pool] _, _ in
|
||||
// guard let pool = pool else {
|
||||
// return
|
||||
// }
|
||||
// pool.recycle(content: content)
|
||||
}) as CFData) else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let image = CGImage(
|
||||
width: Int(self.width),
|
||||
height: Int(self.height),
|
||||
bitsPerComponent: 8,
|
||||
bitsPerPixel: 8 * 4,
|
||||
bytesPerRow: self.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo,
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
shouldInterpolate: true,
|
||||
intent: .defaultIntent
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import ComponentDisplayAdapters
|
|||
import LottieAnimationComponent
|
||||
import ViewControllerComponent
|
||||
import ContextUI
|
||||
import ChatEntityKeyboardInputNode
|
||||
|
||||
enum DrawingToolState: Equatable {
|
||||
enum Key: CaseIterable {
|
||||
|
|
@ -279,10 +280,10 @@ struct DrawingState: Equatable {
|
|||
return DrawingState(
|
||||
selectedTool: .pen,
|
||||
tools: [
|
||||
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.25, mode: .round)),
|
||||
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.5, mode: .round)),
|
||||
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.5, mode: .round)),
|
||||
.pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.5, mode: .round)),
|
||||
.pen(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xe22400), size: 0.2, mode: .round)),
|
||||
.marker(DrawingToolState.BrushState(color: DrawingColor(rgb: 0xfee21b), size: 0.75, mode: .round)),
|
||||
.neon(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x34ffab), size: 0.4, mode: .round)),
|
||||
.pencil(DrawingToolState.BrushState(color: DrawingColor(rgb: 0x2570f0), size: 0.3, mode: .round)),
|
||||
.lasso,
|
||||
.eraser(DrawingToolState.EraserState(size: 0.5, mode: .bitmap))
|
||||
]
|
||||
|
|
@ -308,6 +309,7 @@ enum DrawingScreenTransition {
|
|||
}
|
||||
|
||||
private let undoButtonTag = GenericComponentViewTag()
|
||||
private let redoButtonTag = GenericComponentViewTag()
|
||||
private let clearAllButtonTag = GenericComponentViewTag()
|
||||
private let colorButtonTag = GenericComponentViewTag()
|
||||
private let addButtonTag = GenericComponentViewTag()
|
||||
|
|
@ -699,7 +701,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
func addTextEntity() {
|
||||
let textEntity = DrawingTextEntity(text: "", style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: self.currentColor)
|
||||
let textEntity = DrawingTextEntity(text: NSAttributedString(), style: .regular, font: .sanFrancisco, alignment: .center, fontSize: 1.0, color: DrawingColor(color: .white))
|
||||
self.insertEntity.invoke(textEntity)
|
||||
}
|
||||
|
||||
|
|
@ -811,6 +813,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
style: DrawingTextStyle(style: textEntity.style),
|
||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||
font: DrawingTextFont(font: textEntity.font),
|
||||
isEmojiKeyboard: false,
|
||||
toggleStyle: { [weak state, weak textEntity] in
|
||||
guard let textEntity = textEntity else {
|
||||
return
|
||||
|
|
@ -860,7 +863,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
entityView.update()
|
||||
}
|
||||
state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
},
|
||||
toggleKeyboard: nil
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - 84.0, height: 44.0),
|
||||
transition: context.transition
|
||||
|
|
@ -1200,7 +1204,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
action: {
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
|
@ -1368,37 +1372,40 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
image = nil
|
||||
}
|
||||
|
||||
let brushModeButton = brushModeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BrushButtonContent(
|
||||
title: title,
|
||||
image: image ?? UIImage()
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
guard let controller = controller() as? DrawingScreen else {
|
||||
return
|
||||
}
|
||||
if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View {
|
||||
if isEraser {
|
||||
state?.presentEraserModePicker(buttonView)
|
||||
} else {
|
||||
state?.presentBrushModePicker(buttonView)
|
||||
if [.pen, .eraser].contains(state.drawingState.selectedTool) {
|
||||
let brushModeButton = brushModeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BrushButtonContent(
|
||||
title: title,
|
||||
image: image ?? UIImage()
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
guard let controller = controller() as? DrawingScreen else {
|
||||
return
|
||||
}
|
||||
if let buttonView = controller.node.componentHost.findTaggedView(tag: brushModeButtonTag) as? Button.View {
|
||||
if isEraser {
|
||||
state?.presentEraserModePicker(buttonView)
|
||||
} else {
|
||||
state?.presentBrushModePicker(buttonView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag),
|
||||
availableSize: CGSize(width: 75.0, height: 33.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(brushModeButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel))
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
|
||||
modeRightInset += 35.0
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(brushModeButtonTag),
|
||||
availableSize: CGSize(width: 75.0, height: 33.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(brushModeButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - brushModeButton.size.width / 2.0 - 5.0, y: context.availableSize.height - environment.safeInsets.bottom - brushModeButton.size.height / 2.0 - 2.0 - UIScreenPixel))
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
modeRightInset += 35.0
|
||||
} else {
|
||||
modeRightInset = 16.0
|
||||
}
|
||||
} else {
|
||||
var isFilled = false
|
||||
if let entity = state.selectedEntity as? DrawingSimpleShapeEntity, case .fill = entity.drawType {
|
||||
|
|
@ -1662,7 +1669,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
||||
fileprivate final class Node: ViewControllerTracingNode, FPSCounterDelegate {
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
private weak var controller: DrawingScreen?
|
||||
private let context: AccountContext
|
||||
private let updateState: ActionSlot<DrawingView.NavigationState>
|
||||
|
|
@ -1685,10 +1692,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
private var presentationData: PresentationData
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private let fpsCounter = FPSCounter()
|
||||
private var fpsLabel: UILabel?
|
||||
|
||||
|
||||
private var _drawingView: DrawingView?
|
||||
var drawingView: DrawingView {
|
||||
if self._drawingView == nil, let controller = self.controller {
|
||||
|
|
@ -1741,7 +1745,26 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
}
|
||||
self.performAction.connect { [weak self] action in
|
||||
if let strongSelf = self {
|
||||
strongSelf._drawingView?.performAction(action)
|
||||
if action == .clear {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
self?._drawingView?.performAction(action)
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf._drawingView?.performAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.updateToolState.connect { [weak self] state in
|
||||
|
|
@ -1762,7 +1785,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
private var _entitiesView: DrawingEntitiesView?
|
||||
var entitiesView: DrawingEntitiesView {
|
||||
if self._entitiesView == nil, let controller = self.controller {
|
||||
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size, entities: [])
|
||||
self._entitiesView = DrawingEntitiesView(context: self.context, size: controller.size)
|
||||
self._drawingView?.entitiesView = self._entitiesView
|
||||
let entitiesLayer = self.entitiesView.layer
|
||||
self._drawingView?.getFullImage = { [weak self, weak entitiesLayer] withDrawing in
|
||||
|
|
@ -1917,10 +1940,33 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
super.init()
|
||||
|
||||
self.apply.connect { [weak self] _ in
|
||||
self?.controller?.requestApply()
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.requestApply()
|
||||
}
|
||||
}
|
||||
self.dismiss.connect { [weak self] _ in
|
||||
self?.controller?.requestDismiss()
|
||||
if let strongSelf = self {
|
||||
if !strongSelf.drawingView.isEmpty {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
self?.controller?.requestDismiss()
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.controller?.requestDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1929,23 +1975,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
|
||||
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
if self.fpsLabel == nil {
|
||||
let fpsLabel = UILabel(frame: CGRect(origin: CGPoint(x: 30.0, y: 10.0), size: CGSize(width: 120.0, height: 44.0)))
|
||||
fpsLabel.alpha = 0.1
|
||||
fpsLabel.textColor = .white
|
||||
// self.view.addSubview(fpsLabel)
|
||||
self.fpsLabel = fpsLabel
|
||||
}
|
||||
|
||||
self.fpsCounter.delegate = self
|
||||
self.fpsCounter.startTracking()
|
||||
}
|
||||
}
|
||||
|
||||
func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int) {
|
||||
self.fpsLabel?.text = "\(fps)"
|
||||
}
|
||||
|
||||
func presentEyedropper(dismissed: @escaping () -> Void) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
|
|
@ -2064,6 +2095,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
|
@ -2190,6 +2226,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
style: DrawingTextStyle(style: textEntity.style),
|
||||
alignment: DrawingTextAlignment(alignment: textEntity.alignment),
|
||||
font: DrawingTextFont(font: textEntity.font),
|
||||
isEmojiKeyboard: entityView.textView.inputView != nil,
|
||||
presentColorPicker: { [weak self] in
|
||||
guard let strongSelf = self, let entityView = strongSelf.entitiesView.selectedEntityView as? DrawingTextEntityView, let textEntity = entityView.entity as? DrawingTextEntity else {
|
||||
return
|
||||
|
|
@ -2262,6 +2299,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, transition: .immediate)
|
||||
}
|
||||
},
|
||||
toggleKeyboard: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.toggleInputMode()
|
||||
}
|
||||
)
|
||||
),
|
||||
|
|
@ -2280,6 +2323,58 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleInputMode() {
|
||||
guard let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView else {
|
||||
return
|
||||
}
|
||||
|
||||
let textView = entityView.textView
|
||||
var shouldHaveInputView = false
|
||||
if textView.isFirstResponder {
|
||||
if textView.inputView == nil {
|
||||
shouldHaveInputView = true
|
||||
}
|
||||
} else {
|
||||
shouldHaveInputView = true
|
||||
}
|
||||
|
||||
if shouldHaveInputView {
|
||||
let inputView = EntityInputView(context: self.context, isDark: true, areCustomEmojiEnabled: true)
|
||||
inputView.insertText = { [weak entityView] text in
|
||||
entityView?.insertText(text)
|
||||
}
|
||||
inputView.deleteBackwards = { [weak textView] in
|
||||
textView?.deleteBackward()
|
||||
}
|
||||
inputView.switchToKeyboard = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.toggleInputMode()
|
||||
}
|
||||
// inputView?.presentController = { [weak self] c in
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// strongSelf.presentController(c)
|
||||
// }
|
||||
|
||||
textView.inputView = inputView
|
||||
} else {
|
||||
textView.inputView = nil
|
||||
}
|
||||
|
||||
if textView.isFirstResponder {
|
||||
textView.reloadInputViews()
|
||||
} else {
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, animateOut: false, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var node: Node {
|
||||
|
|
@ -2288,14 +2383,16 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
|
||||
private let context: AccountContext
|
||||
private let size: CGSize
|
||||
private let originalSize: CGSize
|
||||
|
||||
public var requestDismiss: (() -> Void)!
|
||||
public var requestApply: (() -> Void)!
|
||||
public var getCurrentImage: (() -> UIImage?)!
|
||||
|
||||
public init(context: AccountContext, size: CGSize) {
|
||||
public init(context: AccountContext, size: CGSize, originalSize: CGSize) {
|
||||
self.context = context
|
||||
self.size = size
|
||||
self.originalSize = originalSize
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
|
|
@ -2342,36 +2439,36 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
}, opaque: false, scale: 1.0)
|
||||
|
||||
var hasAnimatedEntities = false
|
||||
var legacyEntities: [TGPhotoPaintEntity] = []
|
||||
|
||||
for entity in self.entitiesView.entities {
|
||||
if entity.isAnimated {
|
||||
hasAnimatedEntities = true
|
||||
break
|
||||
}
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let coder = PostboxEncoder()
|
||||
coder.encodeRootObject(entity.file)
|
||||
|
||||
let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38)
|
||||
if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) {
|
||||
stickerEntity.position = entity.position
|
||||
stickerEntity.scale = entity.scale
|
||||
stickerEntity.angle = entity.rotation
|
||||
legacyEntities.append(stickerEntity)
|
||||
}
|
||||
} else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView {
|
||||
let textEntity = TGPhotoPaintStaticEntity()
|
||||
textEntity.position = entity.position
|
||||
textEntity.angle = entity.rotation
|
||||
textEntity.renderImage = view.getRenderImage()
|
||||
legacyEntities.append(textEntity)
|
||||
} else if let _ = entity as? DrawingSimpleShapeEntity {
|
||||
|
||||
} else if let _ = entity as? DrawingBubbleEntity {
|
||||
|
||||
} else if let _ = entity as? DrawingVectorEntity {
|
||||
|
||||
}
|
||||
// if let entity = entity as? DrawingStickerEntity {
|
||||
// let coder = PostboxEncoder()
|
||||
// coder.encodeRootObject(entity.file)
|
||||
//
|
||||
// let baseSize = max(10.0, min(entity.referenceDrawingSize.width, entity.referenceDrawingSize.height) * 0.38)
|
||||
// if let stickerEntity = TGPhotoPaintStickerEntity(document: coder.makeData(), baseSize: CGSize(width: baseSize, height: baseSize), animated: entity.isAnimated) {
|
||||
// stickerEntity.position = entity.position
|
||||
// stickerEntity.scale = entity.scale
|
||||
// stickerEntity.angle = entity.rotation
|
||||
// legacyEntities.append(stickerEntity)
|
||||
// }
|
||||
// } else if let entity = entity as? DrawingTextEntity, let view = self.entitiesView.getView(for: entity.uuid) as? DrawingTextEntityView {
|
||||
// let textEntity = TGPhotoPaintStaticEntity()
|
||||
// textEntity.position = entity.position
|
||||
// textEntity.angle = entity.rotation
|
||||
// textEntity.renderImage = view.getRenderImage()
|
||||
// legacyEntities.append(textEntity)
|
||||
// } else if let _ = entity as? DrawingSimpleShapeEntity {
|
||||
//
|
||||
// } else if let _ = entity as? DrawingBubbleEntity {
|
||||
//
|
||||
// } else if let _ = entity as? DrawingVectorEntity {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
let finalImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in
|
||||
|
|
@ -2396,7 +2493,8 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||
image = finalImage
|
||||
}
|
||||
|
||||
return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil)
|
||||
return TGPaintingData(drawing: nil, entitiesData: self.entitiesView.entitiesData, image: image, stillImage: stillImage, hasAnimation: hasAnimatedEntities)
|
||||
// return TGPaintingData(painting: nil, image: image, stillImage: stillImage, entities: legacyEntities, undoManager: nil)
|
||||
}
|
||||
|
||||
public func resultImage() -> UIImage! {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,26 @@ import UIKit
|
|||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingSimpleShapeEntity: DrawingEntity {
|
||||
public enum ShapeType {
|
||||
final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case shapeType
|
||||
case drawType
|
||||
case color
|
||||
case lineWidth
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
case size
|
||||
case rotation
|
||||
}
|
||||
|
||||
public enum ShapeType: Codable {
|
||||
case rectangle
|
||||
case ellipse
|
||||
case star
|
||||
}
|
||||
|
||||
public enum DrawType {
|
||||
public enum DrawType: Codable {
|
||||
case fill
|
||||
case stroke
|
||||
}
|
||||
|
|
@ -28,6 +40,10 @@ final class DrawingSimpleShapeEntity: DrawingEntity {
|
|||
var size: CGSize
|
||||
var rotation: CGFloat
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
init(shapeType: ShapeType, drawType: DrawType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
|
@ -43,10 +59,33 @@ final class DrawingSimpleShapeEntity: DrawingEntity {
|
|||
self.rotation = 0.0
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
self.isAnimated = false
|
||||
self.shapeType = try container.decode(ShapeType.self, forKey: .shapeType)
|
||||
self.drawType = try container.decode(DrawType.self, forKey: .drawType)
|
||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||
self.size = try container.decode(CGSize.self, forKey: .size)
|
||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.uuid, forKey: .uuid)
|
||||
try container.encode(self.shapeType, forKey: .shapeType)
|
||||
try container.encode(self.drawType, forKey: .drawType)
|
||||
try container.encode(self.color, forKey: .color)
|
||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
try container.encode(self.size, forKey: .size)
|
||||
try container.encode(self.rotation, forKey: .rotation)
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
|
|
|
|||
|
|
@ -8,7 +8,18 @@ import TelegramAnimatedStickerNode
|
|||
import StickerResources
|
||||
import AccountContext
|
||||
|
||||
final class DrawingStickerEntity: DrawingEntity {
|
||||
final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case isAnimated
|
||||
case file
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
case scale
|
||||
case rotation
|
||||
case mirrored
|
||||
}
|
||||
|
||||
let uuid: UUID
|
||||
let isAnimated: Bool
|
||||
let file: TelegramMediaFile
|
||||
|
|
@ -22,6 +33,10 @@ final class DrawingStickerEntity: DrawingEntity {
|
|||
var color: DrawingColor = DrawingColor.clear
|
||||
var lineWidth: CGFloat = 0.0
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
init(file: TelegramMediaFile) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = file.isAnimatedSticker
|
||||
|
|
@ -35,10 +50,30 @@ final class DrawingStickerEntity: DrawingEntity {
|
|||
self.mirrored = false
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
self.isAnimated = try container.decode(Bool.self, forKey: .isAnimated)
|
||||
self.file = try container.decode(TelegramMediaFile.self, forKey: .file)
|
||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||
self.mirrored = try container.decode(Bool.self, forKey: .mirrored)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.uuid, forKey: .uuid)
|
||||
try container.encode(self.isAnimated, forKey: .isAnimated)
|
||||
try container.encode(self.file, forKey: .file)
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
try container.encode(self.scale, forKey: .scale)
|
||||
try container.encode(self.rotation, forKey: .rotation)
|
||||
try container.encode(self.mirrored, forKey: .mirrored)
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingStickerEntity(file: self.file)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
|
|
@ -102,7 +137,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||
|
||||
private func setup() {
|
||||
if let dimensions = self.file.dimensions {
|
||||
if self.file.isAnimatedSticker || self.file.isVideoSticker {
|
||||
if self.file.isAnimatedSticker || self.file.isVideoSticker || self.file.mimeType == "video/webm" {
|
||||
if self.animationNode == nil {
|
||||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||||
animationNode.autoplay = false
|
||||
|
|
@ -176,7 +211,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
|
|||
self.didSetUpAnimationNode = true
|
||||
let dimensions = self.file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker)
|
||||
let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.file.resource, isVideo: self.file.isVideoSticker || self.file.mimeType == "video/webm")
|
||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||
|
||||
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,60 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import EmojiTextAttachmentView
|
||||
|
||||
final class DrawingTextEntity: DrawingEntity {
|
||||
enum Style {
|
||||
final class DrawingTextEntity: DrawingEntity, Codable {
|
||||
final class CustomEmojiAttribute: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case attribute
|
||||
case rangeOrigin
|
||||
case rangeLength
|
||||
}
|
||||
let attribute: ChatTextInputTextCustomEmojiAttribute
|
||||
let range: NSRange
|
||||
|
||||
init(attribute: ChatTextInputTextCustomEmojiAttribute, range: NSRange) {
|
||||
self.attribute = attribute
|
||||
self.range = range
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.attribute = try container.decode(ChatTextInputTextCustomEmojiAttribute.self, forKey: .attribute)
|
||||
|
||||
let rangeOrigin = try container.decode(Int.self, forKey: .rangeOrigin)
|
||||
let rangeLength = try container.decode(Int.self, forKey: .rangeLength)
|
||||
self.range = NSMakeRange(rangeOrigin, rangeLength)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.attribute, forKey: .attribute)
|
||||
try container.encode(self.range.location, forKey: .rangeOrigin)
|
||||
try container.encode(self.range.length, forKey: .rangeLength)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case text
|
||||
case textAttributes
|
||||
case style
|
||||
case font
|
||||
case alignment
|
||||
case fontSize
|
||||
case color
|
||||
case referenceDrawingSize
|
||||
case position
|
||||
case width
|
||||
case scale
|
||||
case rotation
|
||||
}
|
||||
|
||||
enum Style: Codable {
|
||||
case regular
|
||||
case filled
|
||||
case semi
|
||||
|
|
@ -24,7 +74,7 @@ final class DrawingTextEntity: DrawingEntity {
|
|||
}
|
||||
}
|
||||
|
||||
enum Font {
|
||||
enum Font: Codable {
|
||||
case sanFrancisco
|
||||
case newYork
|
||||
case monospaced
|
||||
|
|
@ -44,7 +94,7 @@ final class DrawingTextEntity: DrawingEntity {
|
|||
}
|
||||
}
|
||||
|
||||
enum Alignment {
|
||||
enum Alignment: Codable {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
|
|
@ -73,9 +123,17 @@ final class DrawingTextEntity: DrawingEntity {
|
|||
}
|
||||
|
||||
var uuid: UUID
|
||||
let isAnimated: Bool
|
||||
var isAnimated: Bool {
|
||||
var isAnimated = false
|
||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
isAnimated = true
|
||||
}
|
||||
})
|
||||
return isAnimated
|
||||
}
|
||||
|
||||
var text: String
|
||||
var text: NSAttributedString
|
||||
var style: Style
|
||||
var font: Font
|
||||
var alignment: Alignment
|
||||
|
|
@ -86,11 +144,15 @@ final class DrawingTextEntity: DrawingEntity {
|
|||
var referenceDrawingSize: CGSize
|
||||
var position: CGPoint
|
||||
var width: CGFloat
|
||||
var scale: CGFloat
|
||||
var rotation: CGFloat
|
||||
|
||||
init(text: String, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
}
|
||||
|
||||
init(text: NSAttributedString, style: Style, font: Font, alignment: Alignment, fontSize: CGFloat, color: DrawingColor) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
||||
self.text = text
|
||||
self.style = style
|
||||
|
|
@ -102,18 +164,65 @@ final class DrawingTextEntity: DrawingEntity {
|
|||
self.referenceDrawingSize = .zero
|
||||
self.position = .zero
|
||||
self.width = 100.0
|
||||
self.scale = 1.0
|
||||
self.rotation = 0.0
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.position
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
let text = try container.decode(String.self, forKey: .text)
|
||||
|
||||
let attributedString = NSMutableAttributedString(string: text)
|
||||
let textAttributes = try container.decode([CustomEmojiAttribute].self, forKey: .textAttributes)
|
||||
for attribute in textAttributes {
|
||||
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: attribute.attribute, range: attribute.range)
|
||||
}
|
||||
self.text = attributedString
|
||||
|
||||
self.style = try container.decode(Style.self, forKey: .style)
|
||||
self.font = try container.decode(Font.self, forKey: .font)
|
||||
self.alignment = try container.decode(Alignment.self, forKey: .alignment)
|
||||
self.fontSize = try container.decode(CGFloat.self, forKey: .fontSize)
|
||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||
self.position = try container.decode(CGPoint.self, forKey: .position)
|
||||
self.width = try container.decode(CGFloat.self, forKey: .width)
|
||||
self.scale = try container.decode(CGFloat.self, forKey: .scale)
|
||||
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.uuid, forKey: .uuid)
|
||||
try container.encode(self.text.string, forKey: .text)
|
||||
|
||||
var textAttributes: [CustomEmojiAttribute] = []
|
||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
textAttributes.append(CustomEmojiAttribute(attribute: value, range: range))
|
||||
}
|
||||
})
|
||||
try container.encode(textAttributes, forKey: .textAttributes)
|
||||
|
||||
try container.encode(self.style, forKey: .style)
|
||||
try container.encode(self.font, forKey: .font)
|
||||
try container.encode(self.alignment, forKey: .alignment)
|
||||
try container.encode(self.fontSize, forKey: .fontSize)
|
||||
try container.encode(self.color, forKey: .color)
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.position, forKey: .position)
|
||||
try container.encode(self.width, forKey: .width)
|
||||
try container.encode(self.scale, forKey: .scale)
|
||||
try container.encode(self.rotation, forKey: .rotation)
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
let newEntity = DrawingTextEntity(text: self.text, style: self.style, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color)
|
||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||
newEntity.position = self.position
|
||||
newEntity.width = self.width
|
||||
newEntity.scale = self.scale
|
||||
newEntity.rotation = self.rotation
|
||||
return newEntity
|
||||
}
|
||||
|
|
@ -131,14 +240,15 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
return self.entity as! DrawingTextEntity
|
||||
}
|
||||
|
||||
private let textView: DrawingTextView
|
||||
let textView: DrawingTextView
|
||||
var customEmojiContainerView: CustomEmojiContainerView?
|
||||
var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
init(context: AccountContext, entity: DrawingTextEntity) {
|
||||
self.textView = DrawingTextView(frame: .zero)
|
||||
self.textView.clipsToBounds = false
|
||||
|
||||
self.textView.backgroundColor = .clear
|
||||
self.textView.text = entity.text
|
||||
self.textView.isEditable = false
|
||||
self.textView.isSelectable = false
|
||||
self.textView.contentInset = .zero
|
||||
|
|
@ -159,6 +269,15 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.addSubview(self.textView)
|
||||
|
||||
self.update(animated: false)
|
||||
|
||||
self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
let pointSize: CGFloat = 128.0
|
||||
return EmojiTextAttachmentView(context: context, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -178,9 +297,62 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.endEditing()
|
||||
}
|
||||
|
||||
func updateEntities() {
|
||||
self.textView.drawingLayoutManager.ensureLayout(for: self.textView.textContainer)
|
||||
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
|
||||
if let attributedText = self.textView.attributedText {
|
||||
let beginning = self.textView.beginningOfDocument
|
||||
attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
|
||||
if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = self.textView.position(from: beginning, offset: range.location), let end = self.textView.position(from: start, offset: range.length), let textRange = self.textView.textRange(from: start, to: end) {
|
||||
let rect = self.textView.firstRect(for: textRange)
|
||||
customEmojiRects.append((rect, value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let color = self.textEntity.color.toUIColor()
|
||||
let textColor: UIColor
|
||||
switch self.textEntity.style {
|
||||
case .regular:
|
||||
textColor = color
|
||||
case .filled:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
case .semi:
|
||||
textColor = color
|
||||
case .stroke:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
}
|
||||
|
||||
if !customEmojiRects.isEmpty {
|
||||
let customEmojiContainerView: CustomEmojiContainerView
|
||||
if let current = self.customEmojiContainerView {
|
||||
customEmojiContainerView = current
|
||||
} else {
|
||||
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
||||
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
||||
return nil
|
||||
}
|
||||
return emojiViewProvider(emoji)
|
||||
})
|
||||
customEmojiContainerView.isUserInteractionEnabled = false
|
||||
self.textView.addSubview(customEmojiContainerView)
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(fontSize: self.displayFontSize, textColor: textColor, emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
func beginEditing(accessoryView: UIView?) {
|
||||
self._isEditing = true
|
||||
if !self.textEntity.text.isEmpty {
|
||||
if !self.textEntity.text.string.isEmpty {
|
||||
let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity
|
||||
previousEntity?.uuid = self.textEntity.uuid
|
||||
self.previousEntity = previousEntity
|
||||
|
|
@ -214,7 +386,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
|
||||
if let selectionView = self.selectionView as? DrawingTextEntititySelectionView {
|
||||
selectionView.alpha = 0.0
|
||||
if !self.textEntity.text.isEmpty {
|
||||
if !self.textEntity.text.string.isEmpty {
|
||||
selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
|
@ -223,6 +395,8 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
func endEditing(reset: Bool = false) {
|
||||
self._isEditing = false
|
||||
self.textView.resignFirstResponder()
|
||||
self.textView.inputView = nil
|
||||
self.textView.inputAccessoryView = nil
|
||||
|
||||
self.textView.isEditable = false
|
||||
self.textView.isSelectable = false
|
||||
|
|
@ -240,14 +414,12 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.containerView?.remove(uuid: self.textEntity.uuid)
|
||||
}
|
||||
} else {
|
||||
self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if self.textEntity.text.isEmpty {
|
||||
// self.textEntity.text = self.textView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if self.textEntity.text.string.isEmpty {
|
||||
self.containerView?.remove(uuid: self.textEntity.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
self.textView.text = self.textEntity.text
|
||||
|
||||
|
||||
if let fadeView = self.fadeView {
|
||||
self.fadeView = nil
|
||||
fadeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fadeView] _ in
|
||||
|
|
@ -288,8 +460,33 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else {
|
||||
return
|
||||
}
|
||||
let range = NSMakeRange(0, updatedText.length)
|
||||
updatedText.removeAttribute(.font, range: range)
|
||||
updatedText.removeAttribute(.paragraphStyle, range: range)
|
||||
updatedText.removeAttribute(.foregroundColor, range: range)
|
||||
|
||||
self.textEntity.text = updatedText
|
||||
|
||||
self.sizeToFit()
|
||||
self.textView.setNeedsDisplay()
|
||||
self.update()
|
||||
}
|
||||
|
||||
func insertText(_ text: NSAttributedString) {
|
||||
guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else {
|
||||
return
|
||||
}
|
||||
let range = NSMakeRange(0, updatedText.length)
|
||||
updatedText.removeAttribute(.font, range: range)
|
||||
updatedText.removeAttribute(.paragraphStyle, range: range)
|
||||
updatedText.removeAttribute(.foregroundColor, range: range)
|
||||
updatedText.replaceCharacters(in: self.textView.selectedRange, with: text)
|
||||
|
||||
self.textEntity.text = updatedText
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
|
|
@ -321,27 +518,65 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.textView.frame = rect
|
||||
}
|
||||
|
||||
override func update(animated: Bool = false) {
|
||||
if !self.isEditing {
|
||||
self.center = self.textEntity.position
|
||||
self.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
|
||||
}
|
||||
|
||||
private var displayFontSize: CGFloat {
|
||||
let minFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.05)
|
||||
let maxFontSize = max(10.0, min(self.textEntity.referenceDrawingSize.width, self.textEntity.referenceDrawingSize.height) * 0.45)
|
||||
let fontSize = minFontSize + (maxFontSize - minFontSize) * self.textEntity.fontSize
|
||||
|
||||
return fontSize
|
||||
}
|
||||
|
||||
private func updateText() {
|
||||
guard let text = self.textEntity.text.mutableCopy() as? NSMutableAttributedString else {
|
||||
return
|
||||
}
|
||||
let range = NSMakeRange(0, text.length)
|
||||
let fontSize = self.displayFontSize
|
||||
|
||||
let font: UIFont
|
||||
switch self.textEntity.font {
|
||||
case .sanFrancisco:
|
||||
self.textView.font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||
font = Font.with(size: fontSize, design: .regular, weight: .semibold)
|
||||
case .newYork:
|
||||
self.textView.font = Font.with(size: fontSize, design: .serif, weight: .semibold)
|
||||
font = Font.with(size: fontSize, design: .serif, weight: .semibold)
|
||||
case .monospaced:
|
||||
self.textView.font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
||||
font = Font.with(size: fontSize, design: .monospace, weight: .semibold)
|
||||
case .round:
|
||||
self.textView.font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||
font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||
}
|
||||
text.addAttribute(.font, value: font, range: range)
|
||||
|
||||
let color = self.textEntity.color.toUIColor()
|
||||
let textColor: UIColor
|
||||
switch self.textEntity.style {
|
||||
case .regular:
|
||||
textColor = color
|
||||
case .filled:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
case .semi:
|
||||
textColor = color
|
||||
case .stroke:
|
||||
textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
}
|
||||
text.addAttribute(.foregroundColor, value: textColor, range: range)
|
||||
|
||||
text.enumerateAttributes(in: range) { attributes, subrange, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.customEmoji] {
|
||||
text.addAttribute(.foregroundColor, value: UIColor.clear, range: subrange)
|
||||
}
|
||||
}
|
||||
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = self.textEntity.alignment.alignment
|
||||
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
|
||||
|
||||
self.textView.attributedText = text
|
||||
}
|
||||
|
||||
override func update(animated: Bool = false) {
|
||||
if !self.isEditing {
|
||||
self.center = self.textEntity.position
|
||||
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.textEntity.rotation), self.textEntity.scale, self.textEntity.scale)
|
||||
}
|
||||
self.textView.textAlignment = self.textEntity.alignment.alignment
|
||||
|
||||
let color = self.textEntity.color.toUIColor()
|
||||
switch self.textEntity.style {
|
||||
|
|
@ -358,7 +593,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.textView.strokeColor = nil
|
||||
self.textView.frameColor = UIColor(rgb: 0xffffff, alpha: 0.75)
|
||||
case .stroke:
|
||||
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
self.textView.textColor = color.lightness > 0.99 ? UIColor.black : UIColor.white
|
||||
self.textView.strokeColor = color
|
||||
self.textView.frameColor = nil
|
||||
}
|
||||
|
|
@ -375,22 +610,35 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate {
|
|||
self.textView.layer.shadowRadius = 0.0
|
||||
}
|
||||
|
||||
self.updateText()
|
||||
|
||||
self.sizeToFit()
|
||||
|
||||
Queue.mainQueue().after(0.001) {
|
||||
self.updateEntities()
|
||||
}
|
||||
|
||||
|
||||
super.update(animated: animated)
|
||||
}
|
||||
|
||||
override func updateSelectionView() {
|
||||
super.updateSelectionView()
|
||||
|
||||
guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else {
|
||||
return
|
||||
}
|
||||
self.pushIdentityTransformForMeasurement()
|
||||
|
||||
selectionView.transform = .identity
|
||||
let bounds = self.selectionBounds
|
||||
let center = bounds.center
|
||||
|
||||
// let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
// selectionView.scale = scale
|
||||
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
|
||||
selectionView.center = self.convert(center, to: selectionView.superview)
|
||||
|
||||
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.textEntity.scale) * scale + selectionView.selectionInset * 2.0))
|
||||
selectionView.transform = CGAffineTransformMakeRotation(self.textEntity.rotation)
|
||||
|
||||
self.popIdentityTransformForMeasurement()
|
||||
}
|
||||
|
||||
override func makeSelectionView() -> DrawingEntitySelectionView {
|
||||
|
|
@ -492,7 +740,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||
let delta = gestureRecognizer.translation(in: entityView.superview)
|
||||
let parentLocation = gestureRecognizer.location(in: self.superview)
|
||||
|
||||
var updatedFontSize = entity.fontSize
|
||||
var updatedScale = entity.scale
|
||||
var updatedPosition = entity.position
|
||||
var updatedRotation = entity.rotation
|
||||
|
||||
|
|
@ -502,7 +750,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||
deltaX *= -1.0
|
||||
}
|
||||
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
|
||||
updatedFontSize = max(0.0, min(1.0, updatedFontSize * scaleDelta))
|
||||
updatedScale = max(0.01, updatedScale * scaleDelta)
|
||||
|
||||
let deltaAngle: CGFloat
|
||||
if self.currentHandle === self.leftHandle {
|
||||
|
|
@ -516,7 +764,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||
updatedPosition.y += delta.y
|
||||
}
|
||||
|
||||
entity.fontSize = updatedFontSize
|
||||
entity.scale = updatedScale
|
||||
entity.position = updatedPosition
|
||||
entity.rotation = updatedRotation
|
||||
entityView.update()
|
||||
|
|
@ -537,7 +785,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest
|
|||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
let scale = gestureRecognizer.scale
|
||||
entity.fontSize = max(0.0, min(1.0, entity.fontSize * scale))
|
||||
entity.fontSize = max(0.1, entity.scale * scale)
|
||||
entityView.update()
|
||||
|
||||
gestureRecognizer.scale = 1.0
|
||||
|
|
@ -823,8 +1071,8 @@ private class DrawingTextStorage: NSTextStorage {
|
|||
}
|
||||
}
|
||||
|
||||
private class DrawingTextView: UITextView {
|
||||
var drawingLayoutManager: DrawingTextLayoutManager {
|
||||
class DrawingTextView: UITextView {
|
||||
fileprivate var drawingLayoutManager: DrawingTextLayoutManager {
|
||||
return self.layoutManager as! DrawingTextLayoutManager
|
||||
}
|
||||
|
||||
|
|
@ -912,14 +1160,14 @@ private class DrawingTextView: UITextView {
|
|||
super.insertText(text)
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
self.fixTypingAttributes()
|
||||
super.paste(sender)
|
||||
self.fixTypingAttributes()
|
||||
}
|
||||
|
||||
private func fixTypingAttributes() {
|
||||
fileprivate func fixTypingAttributes() {
|
||||
var attributes: [NSAttributedString.Key: Any] = [:]
|
||||
if let font = self.font {
|
||||
attributes[NSAttributedString.Key.font] = font
|
||||
|
|
|
|||
|
|
@ -6,468 +6,6 @@ protocol DrawingRenderLayer: CALayer {
|
|||
|
||||
}
|
||||
|
||||
//final class PenTool: DrawingElement {
|
||||
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
// var lineWidth: CGFloat = 0.0
|
||||
//
|
||||
// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat, strokeWidth: CGFloat, shadowRadius: CGFloat) {
|
||||
// self.contentsScale = 1.0
|
||||
// self.lineWidth = lineWidth
|
||||
//
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let uuid = UUID()
|
||||
//
|
||||
// let drawingSize: CGSize
|
||||
// let color: DrawingColor
|
||||
// let lineWidth: CGFloat
|
||||
// let arrow: Bool
|
||||
//
|
||||
// var path: BezierPath?
|
||||
// var boundingBox: CGRect?
|
||||
//
|
||||
// var renderPath: CGPath?
|
||||
// let renderStrokeWidth: CGFloat
|
||||
// let renderShadowRadius: CGFloat
|
||||
// let renderLineWidth: CGFloat
|
||||
//
|
||||
// var translation = CGPoint()
|
||||
//
|
||||
// private var currentRenderLayer: DrawingRenderLayer?
|
||||
//
|
||||
// var bounds: CGRect {
|
||||
// return self.path?.path.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
// }
|
||||
//
|
||||
// var points: [Polyline.Point] {
|
||||
// guard let linePath = self.path else {
|
||||
// return []
|
||||
// }
|
||||
// var points: [Polyline.Point] = []
|
||||
// for element in linePath.elements {
|
||||
// if case .moveTo = element.type {
|
||||
// points.append(element.startPoint.offsetBy(self.translation))
|
||||
// } else {
|
||||
// points.append(element.endPoint.offsetBy(self.translation))
|
||||
// }
|
||||
// }
|
||||
// return points
|
||||
// }
|
||||
//
|
||||
// func containsPoint(_ point: CGPoint) -> Bool {
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
// }
|
||||
//
|
||||
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
// if let linePath = self.path {
|
||||
// let pathBoundingBox = path.bounds
|
||||
// if self.bounds.intersects(pathBoundingBox) {
|
||||
// for element in linePath.elements {
|
||||
// if case .moveTo = element.type {
|
||||
// if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// } else {
|
||||
// if path.contains(element.startPoint.location.offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// if path.contains(element.endPoint.location.offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// if case .cubicCurve = element.type {
|
||||
// if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// if path.contains(element.controlPoints[1].offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// } else if case .quadCurve = element.type {
|
||||
// if path.contains(element.controlPoints[0].offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
// self.drawingSize = drawingSize
|
||||
// self.color = color
|
||||
// self.lineWidth = lineWidth
|
||||
// self.arrow = arrow
|
||||
//
|
||||
// let strokeWidth = min(drawingSize.width, drawingSize.height) * 0.008
|
||||
// let shadowRadius = min(drawingSize.width, drawingSize.height) * 0.03
|
||||
//
|
||||
// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003)
|
||||
// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09)
|
||||
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
//
|
||||
// self.renderStrokeWidth = strokeWidth
|
||||
// self.renderShadowRadius = shadowRadius
|
||||
// self.renderLineWidth = lineWidth
|
||||
// }
|
||||
//
|
||||
// func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
// let layer = RenderLayer()
|
||||
// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.renderLineWidth, strokeWidth: self.renderStrokeWidth, shadowRadius: self.renderShadowRadius)
|
||||
// self.currentRenderLayer = layer
|
||||
// return layer
|
||||
// }
|
||||
//
|
||||
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
// guard case let .smoothCurve(bezierPath) = path else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.path = bezierPath
|
||||
//
|
||||
//// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
|
||||
//// let lastPoint = lastPosition
|
||||
//// var secondPoint = polyline.points[polyline.points.count - 2]
|
||||
//// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
|
||||
//// secondPoint = polyline.points[polyline.points.count - 3]
|
||||
//// }
|
||||
//// let angle = lastPoint.angle(to: secondPoint.location)
|
||||
//// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
|
||||
//// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
|
||||
////
|
||||
//// let arrowPath = UIBezierPath()
|
||||
//// arrowPath.move(to: point2)
|
||||
//// arrowPath.addLine(to: lastPoint)
|
||||
//// arrowPath.addLine(to: point1)
|
||||
//// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
////
|
||||
//// combinedPath.usesEvenOddFillRule = false
|
||||
//// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
|
||||
//// }
|
||||
//
|
||||
//
|
||||
// let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
// self.renderPath = cgPath
|
||||
//
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.updatePath(cgPath)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func draw(in context: CGContext, size: CGSize) {
|
||||
// guard let path = self.renderPath else {
|
||||
// return
|
||||
// }
|
||||
// context.saveGState()
|
||||
//
|
||||
// context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
//
|
||||
// context.setShouldAntialias(true)
|
||||
//
|
||||
// context.setBlendMode(.normal)
|
||||
//
|
||||
// context.addPath(path)
|
||||
// context.setFillColor(UIColor.white.cgColor)
|
||||
// context.setStrokeColor(UIColor.white.cgColor)
|
||||
// context.setLineWidth(self.renderStrokeWidth * 0.5)
|
||||
// context.setShadow(offset: .zero, blur: self.renderShadowRadius * 3.0, color: self.color.toCGColor())
|
||||
// context.drawPath(using: .fillStroke)
|
||||
//
|
||||
// context.addPath(path)
|
||||
// context.setShadow(offset: .zero, blur: 0.0, color: UIColor.clear.cgColor)
|
||||
// context.setLineCap(.round)
|
||||
// context.setLineWidth(self.renderStrokeWidth)
|
||||
// context.setStrokeColor(UIColor.white.mixedWith(self.color.toUIColor(), alpha: 0.25).cgColor)
|
||||
// context.strokePath()
|
||||
//
|
||||
// context.addPath(path)
|
||||
// context.setFillColor(UIColor.white.cgColor)
|
||||
//
|
||||
// context.fillPath()
|
||||
//
|
||||
// context.restoreGState()
|
||||
// }
|
||||
//}
|
||||
|
||||
//final class PenTool: DrawingElement {
|
||||
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
// var lineWidth: CGFloat = 0.0
|
||||
//
|
||||
// let fillLayer = SimpleShapeLayer()
|
||||
//
|
||||
// func setup(size: CGSize, color: DrawingColor, lineWidth: CGFloat) {
|
||||
// self.contentsScale = 1.0
|
||||
//
|
||||
// let minLineWidth = max(1.0, min(size.width, size.height) * 0.003)
|
||||
// let maxLineWidth = max(10.0, min(size.width, size.height) * 0.055)
|
||||
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
//
|
||||
// self.lineWidth = lineWidth
|
||||
//
|
||||
// let bounds = CGRect(origin: .zero, size: size)
|
||||
// self.frame = bounds
|
||||
//
|
||||
// self.fillLayer.frame = bounds
|
||||
// self.fillLayer.contentsScale = 1.0
|
||||
// self.fillLayer.fillColor = color.toCGColor()
|
||||
// self.fillLayer.strokeColor = color.toCGColor()
|
||||
// self.fillLayer.lineWidth = 1.0
|
||||
//
|
||||
// self.addSublayer(self.fillLayer)
|
||||
// }
|
||||
//
|
||||
// func updatePath(_ path: CGPath) {
|
||||
// self.fillLayer.path = path
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let uuid = UUID()
|
||||
//
|
||||
// let drawingSize: CGSize
|
||||
// let color: DrawingColor
|
||||
// let lineWidth: CGFloat
|
||||
// let arrow: Bool
|
||||
//
|
||||
// var polyline: Polyline?
|
||||
//// var path = BezierPath()
|
||||
// let renderArrowLength: CGFloat
|
||||
// let renderArrowLineWidth: CGFloat
|
||||
// var renderLineWidth: CGFloat = 0.0
|
||||
//
|
||||
// var translation = CGPoint()
|
||||
//
|
||||
// private var currentRenderLayer: DrawingRenderLayer?
|
||||
//
|
||||
// var bounds: CGRect {
|
||||
// return .zero
|
||||
// }
|
||||
//
|
||||
// var points: [CGPoint] {
|
||||
// guard let polyline = self.polyline else {
|
||||
// return []
|
||||
// }
|
||||
// var points: [CGPoint] = []
|
||||
// for point in polyline.points {
|
||||
// points.append(point.location)
|
||||
// }
|
||||
// return points
|
||||
// }
|
||||
//
|
||||
// func containsPoint(_ point: CGPoint) -> Bool {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
// if let polyline = self.polyline {
|
||||
// let pathBoundingBox = path.bounds
|
||||
// if self.bounds.intersects(pathBoundingBox) {
|
||||
// for point in polyline.points {
|
||||
// if path.contains(point.location) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
// self.drawingSize = drawingSize
|
||||
// self.color = color
|
||||
// self.lineWidth = lineWidth
|
||||
// self.arrow = arrow
|
||||
//
|
||||
// self.renderArrowLength = min(drawingSize.width, drawingSize.height) * 0.04
|
||||
// self.renderArrowLineWidth = min(drawingSize.width, drawingSize.height) * 0.01
|
||||
// }
|
||||
//
|
||||
// func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
// let layer = RenderLayer()
|
||||
// layer.setup(size: self.drawingSize, color: self.color, lineWidth: self.lineWidth)
|
||||
// self.currentRenderLayer = layer
|
||||
// return layer
|
||||
// }
|
||||
//
|
||||
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
// guard case let .polyline(polyline) = path else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.polyline = polyline
|
||||
//
|
||||
// struct LineSegment {
|
||||
// var firstPoint: CGPoint
|
||||
// var secondPoint: CGPoint
|
||||
// }
|
||||
//
|
||||
// func lineSegmentPerpendicularTo(_ pp: LineSegment, fraction: CGFloat) -> LineSegment {
|
||||
// let x0: CGFloat = pp.firstPoint.x
|
||||
// let y0: CGFloat = pp.firstPoint.y
|
||||
//
|
||||
// let x1: CGFloat = pp.secondPoint.x
|
||||
// let y1: CGFloat = pp.secondPoint.y
|
||||
//
|
||||
// let dx = x1 - x0
|
||||
// let dy = y1 - y0
|
||||
//
|
||||
// var xa: CGFloat
|
||||
// var ya: CGFloat
|
||||
// var xb: CGFloat
|
||||
// var yb: CGFloat
|
||||
//
|
||||
// xa = x1 + fraction * 0.5 * dy
|
||||
// ya = y1 - fraction * 0.5 * dx
|
||||
// xb = x1 - fraction * 0.5 * dy
|
||||
// yb = y1 + fraction * 0.5 * dx
|
||||
//
|
||||
// return LineSegment(firstPoint: CGPoint(x: xa, y: ya), secondPoint: CGPoint(x: xb, y: yb))
|
||||
// }
|
||||
//
|
||||
//
|
||||
// func len_sq(_ p1: CGPoint, p2: CGPoint) -> CGFloat {
|
||||
// let dx: CGFloat = p2.x - p1.x
|
||||
// let dy: CGFloat = p2.y - p1.y
|
||||
// return dx * dx + dy * dy
|
||||
// }
|
||||
//
|
||||
// func clamp<T: Comparable>(_ value: T, min: T, max: T) -> T {
|
||||
// if (value < min) {
|
||||
// return min
|
||||
// }
|
||||
// if (value > max) {
|
||||
// return max
|
||||
// }
|
||||
// return value
|
||||
// }
|
||||
//
|
||||
// var ls: [LineSegment] = []
|
||||
// var isFirst = true
|
||||
//
|
||||
// let combinedPath = UIBezierPath()
|
||||
// var segmentPath = UIBezierPath()
|
||||
//
|
||||
// var pts: [CGPoint] = []
|
||||
// var ctr: Int = 0
|
||||
//
|
||||
// let ff: CGFloat = 0.2
|
||||
// let lower: CGFloat = 0.01
|
||||
// let upper: CGFloat = 1.0
|
||||
//
|
||||
// var lastUsedIndex = 0
|
||||
// var index = 0
|
||||
// for point in polyline.points {
|
||||
// pts.insert(point.location, at: ctr)
|
||||
// ctr += 1
|
||||
//
|
||||
// if ctr == 5 {
|
||||
// pts[3] = CGPoint(x: (pts[2].x + pts[4].x) / 2.0, y: (pts[2].y + pts[4].y) / 2.0)
|
||||
//
|
||||
// if isFirst {
|
||||
// isFirst = false
|
||||
// let segment = LineSegment(firstPoint: pts[0], secondPoint: pts[0])
|
||||
// ls.append(segment)
|
||||
// segmentPath.move(to: pts[0])
|
||||
// }
|
||||
//
|
||||
// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper)
|
||||
// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: pts[2]), min: lower, max: upper)
|
||||
// let frac3: CGFloat = ff/clamp(len_sq(pts[2], p2: pts[3]), min: lower, max: upper)
|
||||
//
|
||||
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1)
|
||||
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: pts[2]), fraction: frac2), at: 2)
|
||||
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[2], secondPoint: pts[3]), fraction: frac3), at: 3)
|
||||
//
|
||||
// segmentPath.move(to: ls[0].firstPoint)
|
||||
// segmentPath.addCurve(to: ls[3].firstPoint, controlPoint1: ls[1].firstPoint, controlPoint2: ls[2].firstPoint)
|
||||
// segmentPath.addLine(to: ls[3].secondPoint)
|
||||
// segmentPath.addCurve(to: ls[0].secondPoint, controlPoint1: ls[2].secondPoint, controlPoint2: ls[1].secondPoint)
|
||||
// segmentPath.close()
|
||||
// combinedPath.append(segmentPath)
|
||||
//
|
||||
// let last = ls[3]
|
||||
// ls.removeAll()
|
||||
// ls.append(last)
|
||||
//
|
||||
// pts[0] = pts[3]
|
||||
// pts[1] = pts[4]
|
||||
// ctr = 2
|
||||
//
|
||||
// combinedPath.append(segmentPath)
|
||||
// segmentPath = UIBezierPath()
|
||||
//
|
||||
// lastUsedIndex = index
|
||||
// }
|
||||
// index += 1
|
||||
// }
|
||||
//
|
||||
// var lastPosition = polyline.points.last?.location ?? CGPoint()
|
||||
// if let lastPoint = polyline.points.last, ls.count > 0 {
|
||||
// if lastUsedIndex < polyline.points.count - 1 {
|
||||
// let frac1: CGFloat = ff/clamp(len_sq(pts[0], p2: pts[1]), min: lower, max: upper)
|
||||
// let frac2: CGFloat = ff/clamp(len_sq(pts[1], p2: lastPoint.location), min: lower, max: upper)
|
||||
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[0], secondPoint: pts[1]), fraction: frac1), at: 1)
|
||||
// ls.insert(lineSegmentPerpendicularTo(LineSegment(firstPoint: pts[1], secondPoint: lastPoint.location), fraction: frac2), at: 2)
|
||||
//
|
||||
// segmentPath.move(to: ls[0].firstPoint)
|
||||
// segmentPath.addQuadCurve(to: ls[2].firstPoint, controlPoint: ls[1].firstPoint)
|
||||
// segmentPath.addLine(to: ls[2].secondPoint)
|
||||
// segmentPath.addQuadCurve(to: ls[0].secondPoint, controlPoint: ls[1].secondPoint)
|
||||
// segmentPath.addLine(to: ls[0].secondPoint)
|
||||
// segmentPath.close()
|
||||
// combinedPath.append(segmentPath)
|
||||
//
|
||||
// let diameter = ls[2].firstPoint.distance(to: ls[2].secondPoint)
|
||||
// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: lastPoint.x - diameter * 0.5, y: lastPoint.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter))))
|
||||
//
|
||||
// lastPosition = lastPoint.location
|
||||
// } else {
|
||||
// let diameter = ls[0].firstPoint.distance(to: ls[0].secondPoint)
|
||||
// let center = ls[0].firstPoint.point(to: ls[0].secondPoint, t: 0.5)
|
||||
// combinedPath.append(UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: center.x - diameter * 0.5, y: center.y - diameter * 0.5), size: CGSize(width: diameter, height: diameter))))
|
||||
//
|
||||
// lastPosition = center
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
|
||||
// let lastPoint = lastPosition
|
||||
// var secondPoint = polyline.points[polyline.points.count - 2]
|
||||
// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
|
||||
// secondPoint = polyline.points[polyline.points.count - 3]
|
||||
// }
|
||||
// let angle = lastPoint.angle(to: secondPoint.location)
|
||||
// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
|
||||
// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
|
||||
//
|
||||
// let arrowPath = UIBezierPath()
|
||||
// arrowPath.move(to: point2)
|
||||
// arrowPath.addLine(to: lastPoint)
|
||||
// arrowPath.addLine(to: point1)
|
||||
// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
//
|
||||
// combinedPath.usesEvenOddFillRule = false
|
||||
// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
|
||||
// }
|
||||
//
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.updatePath(combinedPath.cgPath)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func draw(in context: CGContext, size: CGSize) {
|
||||
// let renderLayer: DrawingRenderLayer?
|
||||
// if let currentRenderLayer = self.currentRenderLayer {
|
||||
// renderLayer = currentRenderLayer
|
||||
// } else {
|
||||
// renderLayer = self.setupRenderLayer()
|
||||
//// (renderLayer as? RenderLayer)?.updatePath(self.path.path.cgPath)
|
||||
// }
|
||||
// renderLayer?.render(in: context)
|
||||
// }
|
||||
//}
|
||||
|
||||
final class MarkerTool: DrawingElement {
|
||||
let uuid = UUID()
|
||||
|
||||
|
|
@ -483,22 +21,25 @@ final class MarkerTool: DrawingElement {
|
|||
var translation = CGPoint()
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.renderPath.bounds
|
||||
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] = []
|
||||
|
||||
var _points: [Polyline.Point] = []
|
||||
var points: [Polyline.Point] {
|
||||
return self._points.map { $0.offsetBy(self.translation) }
|
||||
}
|
||||
|
||||
weak var metalView: DrawingMetalView?
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
return self.renderPath.contains(point)
|
||||
return self.renderPath.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y)))
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in self.points {
|
||||
if path.contains(point.location) {
|
||||
for point in self._points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -523,27 +64,43 @@ final class MarkerTool: DrawingElement {
|
|||
return nil
|
||||
}
|
||||
|
||||
private var hot = false
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .location(point) = path else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.points.isEmpty {
|
||||
|
||||
if self._points.isEmpty {
|
||||
self.renderPath.move(to: point.location)
|
||||
} else {
|
||||
self.renderPath.addLine(to: point.location)
|
||||
}
|
||||
self.points.append(point)
|
||||
self._points.append(point)
|
||||
|
||||
self.hot = true
|
||||
self.metalView?.updated(point, state: state, brush: .marker, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
guard !self.points.isEmpty else {
|
||||
guard !self._points.isEmpty else {
|
||||
return
|
||||
}
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
let hot = self.hot
|
||||
if hot {
|
||||
self.hot = false
|
||||
} else {
|
||||
self.metalView?.setup(self._points.map { $0.location }, brush: .marker, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
self.metalView?.drawInContext(context)
|
||||
if !hot {
|
||||
self.metalView?.clear()
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -789,10 +346,13 @@ final class PencilTool: DrawingElement {
|
|||
var renderAngle: CGFloat = 0.0
|
||||
|
||||
var bounds: CGRect {
|
||||
return self.renderPath.bounds
|
||||
return self.renderPath.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y)
|
||||
}
|
||||
|
||||
var points: [Polyline.Point] = []
|
||||
var _points: [Polyline.Point] = []
|
||||
var points: [Polyline.Point] {
|
||||
return self._points.map { $0.offsetBy(self.translation) }
|
||||
}
|
||||
|
||||
weak var metalView: DrawingMetalView?
|
||||
|
||||
|
|
@ -803,7 +363,7 @@ final class PencilTool: DrawingElement {
|
|||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
let pathBoundingBox = path.bounds
|
||||
if self.bounds.intersects(pathBoundingBox) {
|
||||
for point in self.points {
|
||||
for point in self._points {
|
||||
if path.contains(point.location.offsetBy(self.translation)) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -829,27 +389,43 @@ final class PencilTool: DrawingElement {
|
|||
return nil
|
||||
}
|
||||
|
||||
private var hot = false
|
||||
func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
guard case let .location(point) = path else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.points.isEmpty {
|
||||
if self._points.isEmpty {
|
||||
self.renderPath.move(to: point.location)
|
||||
} else {
|
||||
self.renderPath.addLine(to: point.location)
|
||||
}
|
||||
self.points.append(point)
|
||||
self._points.append(point)
|
||||
|
||||
self.hot = true
|
||||
self.metalView?.updated(point, state: state, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
|
||||
func draw(in context: CGContext, size: CGSize) {
|
||||
guard !self.points.isEmpty else {
|
||||
guard !self._points.isEmpty else {
|
||||
return
|
||||
}
|
||||
context.saveGState()
|
||||
|
||||
context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
|
||||
let hot = self.hot
|
||||
if hot {
|
||||
self.hot = false
|
||||
} else {
|
||||
self.metalView?.setup(self._points.map { $0.location }, brush: .pencil, color: self.color, size: self.renderLineWidth)
|
||||
}
|
||||
self.metalView?.drawInContext(context)
|
||||
if !hot {
|
||||
self.metalView?.clear()
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ import UIKit
|
|||
import QuartzCore
|
||||
import simd
|
||||
|
||||
struct DrawingColor: Equatable {
|
||||
struct DrawingColor: Equatable, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case red
|
||||
case green
|
||||
case blue
|
||||
case alpha
|
||||
case position
|
||||
}
|
||||
|
||||
public static var clear = DrawingColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
|
||||
public var red: CGFloat
|
||||
|
|
@ -48,6 +56,24 @@ struct DrawingColor: Equatable {
|
|||
public init(rgb: UInt32) {
|
||||
self.init(color: UIColor(rgb: rgb))
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.red = try container.decode(CGFloat.self, forKey: .red)
|
||||
self.green = try container.decode(CGFloat.self, forKey: .green)
|
||||
self.blue = try container.decode(CGFloat.self, forKey: .blue)
|
||||
self.alpha = try container.decode(CGFloat.self, forKey: .alpha)
|
||||
self.position = try container.decodeIfPresent(CGPoint.self, forKey: .position)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.red, forKey: .red)
|
||||
try container.encode(self.green, forKey: .green)
|
||||
try container.encode(self.blue, forKey: .blue)
|
||||
try container.encode(self.alpha, forKey: .alpha)
|
||||
try container.encodeIfPresent(self.position, forKey: .position)
|
||||
}
|
||||
|
||||
func withUpdatedRed(_ red: CGFloat) -> DrawingColor {
|
||||
return DrawingColor(
|
||||
|
|
@ -561,156 +587,6 @@ private func configureControlPoints(data: [CGPoint]) -> [(ctrl1: CGPoint, ctrl2:
|
|||
return []
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class FPSCounter: NSObject {
|
||||
|
||||
/// Helper class that relays display link updates to the FPSCounter
|
||||
///
|
||||
/// This is necessary because CADisplayLink retains its target. Thus
|
||||
/// if the FPSCounter class would be the target of the display link
|
||||
/// it would create a retain cycle. The delegate has a weak reference
|
||||
/// to its parent FPSCounter, thus preventing this.
|
||||
///
|
||||
internal class DisplayLinkProxy: NSObject {
|
||||
|
||||
/// A weak ref to the parent FPSCounter instance.
|
||||
@objc weak var parentCounter: FPSCounter?
|
||||
|
||||
/// Notify the parent FPSCounter of a CADisplayLink update.
|
||||
///
|
||||
/// This method is automatically called by the CADisplayLink.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - displayLink: The display link that updated
|
||||
///
|
||||
@objc func updateFromDisplayLink(_ displayLink: CADisplayLink) {
|
||||
parentCounter?.updateFromDisplayLink(displayLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private let displayLink: CADisplayLink
|
||||
private let displayLinkProxy: DisplayLinkProxy
|
||||
|
||||
/// Create a new FPSCounter.
|
||||
///
|
||||
/// To start receiving FPS updates you need to start tracking with the
|
||||
/// `startTracking(inRunLoop:mode:)` method.
|
||||
///
|
||||
public override init() {
|
||||
self.displayLinkProxy = DisplayLinkProxy()
|
||||
self.displayLink = CADisplayLink(
|
||||
target: self.displayLinkProxy,
|
||||
selector: #selector(DisplayLinkProxy.updateFromDisplayLink(_:))
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.displayLinkProxy.parentCounter = self
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.displayLink.invalidate()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
/// The delegate that should receive FPS updates.
|
||||
public weak var delegate: FPSCounterDelegate?
|
||||
|
||||
/// Delay between FPS updates. Longer delays mean more averaged FPS numbers.
|
||||
@objc public var notificationDelay: TimeInterval = 1.0
|
||||
|
||||
|
||||
// MARK: - Tracking
|
||||
|
||||
private var runloop: RunLoop?
|
||||
private var mode: RunLoop.Mode?
|
||||
|
||||
/// Start tracking FPS updates.
|
||||
///
|
||||
/// You can specify wich runloop to use for tracking, as well as the runloop modes.
|
||||
/// Usually you'll want the main runloop (default), and either the common run loop modes
|
||||
/// (default), or the tracking mode (`RunLoop.Mode.tracking`).
|
||||
///
|
||||
/// When the counter is already tracking, it's stopped first.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - runloop: The runloop to start tracking in
|
||||
/// - mode: The mode(s) to track in the runloop
|
||||
///
|
||||
@objc public func startTracking(inRunLoop runloop: RunLoop = .main, mode: RunLoop.Mode = .common) {
|
||||
self.stopTracking()
|
||||
|
||||
self.runloop = runloop
|
||||
self.mode = mode
|
||||
self.displayLink.add(to: runloop, forMode: mode)
|
||||
}
|
||||
|
||||
/// Stop tracking FPS updates.
|
||||
///
|
||||
/// This method does nothing if the counter is not currently tracking.
|
||||
///
|
||||
@objc public func stopTracking() {
|
||||
guard let runloop = self.runloop, let mode = self.mode else { return }
|
||||
|
||||
self.displayLink.remove(from: runloop, forMode: mode)
|
||||
self.runloop = nil
|
||||
self.mode = nil
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Handling Frame Updates
|
||||
|
||||
private var lastNotificationTime: CFAbsoluteTime = 0.0
|
||||
private var numberOfFrames = 0
|
||||
|
||||
private func updateFromDisplayLink(_ displayLink: CADisplayLink) {
|
||||
if self.lastNotificationTime == 0.0 {
|
||||
self.lastNotificationTime = CFAbsoluteTimeGetCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
self.numberOfFrames += 1
|
||||
|
||||
let currentTime = CFAbsoluteTimeGetCurrent()
|
||||
let elapsedTime = currentTime - self.lastNotificationTime
|
||||
|
||||
if elapsedTime >= self.notificationDelay {
|
||||
self.notifyUpdateForElapsedTime(elapsedTime)
|
||||
self.lastNotificationTime = 0.0
|
||||
self.numberOfFrames = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyUpdateForElapsedTime(_ elapsedTime: CFAbsoluteTime) {
|
||||
let fps = Int(round(Double(self.numberOfFrames) / elapsedTime))
|
||||
self.delegate?.fpsCounter(self, didUpdateFramesPerSecond: fps)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The delegate protocol for the FPSCounter class.
|
||||
///
|
||||
/// Implement this protocol if you want to receive updates from a `FPSCounter`.
|
||||
///
|
||||
protocol FPSCounterDelegate: NSObjectProtocol {
|
||||
|
||||
/// Called in regular intervals while the counter is tracking FPS.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - counter: The FPSCounter that sent the update
|
||||
/// - fps: The current FPS of the application
|
||||
///
|
||||
func fpsCounter(_ counter: FPSCounter, didUpdateFramesPerSecond fps: Int)
|
||||
}
|
||||
|
||||
class BezierPath {
|
||||
struct Element {
|
||||
enum ElementType {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,20 @@ import UIKit
|
|||
import Display
|
||||
import AccountContext
|
||||
|
||||
final class DrawingVectorEntity: DrawingEntity {
|
||||
public enum VectorType {
|
||||
final class DrawingVectorEntity: DrawingEntity, Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case type
|
||||
case color
|
||||
case lineWidth
|
||||
case drawingSize
|
||||
case referenceDrawingSize
|
||||
case start
|
||||
case mid
|
||||
case end
|
||||
}
|
||||
|
||||
public enum VectorType: Codable {
|
||||
case line
|
||||
case oneSidedArrow
|
||||
case twoSidedArrow
|
||||
|
|
@ -34,6 +46,10 @@ final class DrawingVectorEntity: DrawingEntity {
|
|||
}
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.start
|
||||
}
|
||||
|
||||
init(type: VectorType, color: DrawingColor, lineWidth: CGFloat) {
|
||||
self.uuid = UUID()
|
||||
self.isAnimated = false
|
||||
|
|
@ -49,8 +65,34 @@ final class DrawingVectorEntity: DrawingEntity {
|
|||
self.end = CGPoint()
|
||||
}
|
||||
|
||||
var center: CGPoint {
|
||||
return self.start
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
self.isAnimated = false
|
||||
self.type = try container.decode(VectorType.self, forKey: .type)
|
||||
self.color = try container.decode(DrawingColor.self, forKey: .color)
|
||||
self.lineWidth = try container.decode(CGFloat.self, forKey: .lineWidth)
|
||||
self.drawingSize = try container.decode(CGSize.self, forKey: .drawingSize)
|
||||
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
|
||||
self.start = try container.decode(CGPoint.self, forKey: .start)
|
||||
|
||||
let mid = try container.decode(CGPoint.self, forKey: .mid)
|
||||
self.mid = (mid.x, mid.y)
|
||||
|
||||
self.end = try container.decode(CGPoint.self, forKey: .end)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.uuid, forKey: .uuid)
|
||||
try container.encode(self.type, forKey: .type)
|
||||
try container.encode(self.color, forKey: .color)
|
||||
try container.encode(self.lineWidth, forKey: .lineWidth)
|
||||
try container.encode(self.drawingSize, forKey: .drawingSize)
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
try container.encode(self.start, forKey: .start)
|
||||
try container.encode(CGPoint(x: self.mid.0, y: self.mid.1), forKey: .mid)
|
||||
try container.encode(self.end, forKey: .end)
|
||||
}
|
||||
|
||||
func duplicate() -> DrawingEntity {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
private var currentDrawingView: UIView
|
||||
private var currentDrawingLayer: DrawingRenderLayer?
|
||||
|
||||
private var selectionImage: UIImage?
|
||||
private var pannedSelectionView: UIView
|
||||
|
||||
var lassoView: DrawingLassoView
|
||||
|
|
@ -155,6 +154,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
self.lassoView.isHidden = true
|
||||
|
||||
self.metalView = DrawingMetalView(size: size)!
|
||||
self.metalView.isHidden = true
|
||||
|
||||
self.brushSizePreviewLayer = SimpleShapeLayer()
|
||||
self.brushSizePreviewLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 100.0, height: 100.0))
|
||||
|
|
@ -258,6 +258,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
guard let newElement = strongSelf.prepareNewElement() else {
|
||||
return
|
||||
}
|
||||
|
||||
if newElement is MarkerTool || newElement is PencilTool {
|
||||
self?.metalView.isHidden = false
|
||||
}
|
||||
|
||||
if let renderLayer = newElement.setupRenderLayer() {
|
||||
strongSelf.currentDrawingView.layer.addSublayer(renderLayer)
|
||||
strongSelf.currentDrawingLayer = renderLayer
|
||||
|
|
@ -366,7 +371,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
if let strongSelf = self {
|
||||
strongSelf.skipDrawing = Set(elements)
|
||||
strongSelf.commit(reset: true)
|
||||
strongSelf.updateSelectionImage()
|
||||
strongSelf.updateSelectionContent()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,12 +391,12 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
}
|
||||
}
|
||||
strongSelf.skipDrawing = Set()
|
||||
strongSelf.commit(reset: true)
|
||||
strongSelf.selectionImage = nil
|
||||
strongSelf.pannedSelectionView.layer.contents = nil
|
||||
|
||||
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
|
||||
strongSelf.lassoView.translate(offset)
|
||||
strongSelf.commit(reset: true, completion: {
|
||||
strongSelf.pannedSelectionView.layer.contents = nil
|
||||
|
||||
strongSelf.lassoView.bounds = CGRect(origin: .zero, size: strongSelf.lassoView.bounds.size)
|
||||
strongSelf.lassoView.translate(offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -468,46 +473,66 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
}
|
||||
}
|
||||
|
||||
private let queue = Queue()
|
||||
private var skipDrawing = Set<UUID>()
|
||||
private func commit(reset: Bool = false) {
|
||||
private func commit(reset: Bool = false, interactive: Bool = false, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
let currentImage = self.drawingImage
|
||||
self.drawingImage = self.renderer.image { context in
|
||||
if !reset {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
|
||||
if let image = currentImage {
|
||||
image.draw(at: .zero)
|
||||
}
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
uncommitedElement.draw(in: context.cgContext, size: self.imageSize)
|
||||
}
|
||||
} else {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: self.imageSize))
|
||||
for element in self.elements {
|
||||
if !self.skipDrawing.contains(element.uuid) {
|
||||
element.draw(in: context.cgContext, size: self.imageSize)
|
||||
let uncommitedElement = self.uncommitedElement
|
||||
let imageSize = self.imageSize
|
||||
let skipDrawing = self.skipDrawing
|
||||
|
||||
let action = {
|
||||
let updatedImage = self.renderer.image { context in
|
||||
if !reset {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: imageSize))
|
||||
if let image = currentImage {
|
||||
image.draw(at: .zero)
|
||||
}
|
||||
if let uncommitedElement = uncommitedElement {
|
||||
uncommitedElement.draw(in: context.cgContext, size: imageSize)
|
||||
}
|
||||
} else {
|
||||
context.cgContext.clear(CGRect(origin: .zero, size: imageSize))
|
||||
for element in self.elements {
|
||||
if !skipDrawing.contains(element.uuid) {
|
||||
element.draw(in: context.cgContext, size: imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Queue.mainQueue().async {
|
||||
self.drawingImage = updatedImage
|
||||
self.layer.contents = updatedImage.cgImage
|
||||
|
||||
if let currentDrawingLayer = self.currentDrawingLayer {
|
||||
self.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
self.metalView.clear()
|
||||
self.metalView.isHidden = true
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
self.layer.contents = self.drawingImage?.cgImage
|
||||
|
||||
if let currentDrawingLayer = self.currentDrawingLayer {
|
||||
self.currentDrawingLayer = nil
|
||||
currentDrawingLayer.removeFromSuperlayer()
|
||||
if synchronous {
|
||||
action()
|
||||
} else {
|
||||
self.queue.async {
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
self.metalView.clear()
|
||||
}
|
||||
|
||||
private func updateSelectionImage() {
|
||||
self.selectionImage = self.renderer.image { context in
|
||||
private func updateSelectionContent() {
|
||||
let selectionImage = self.renderer.image { context in
|
||||
for element in self.elements {
|
||||
if self.skipDrawing.contains(element.uuid) {
|
||||
element.draw(in: context.cgContext, size: self.imageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.pannedSelectionView.layer.contents = self.selectionImage?.cgImage
|
||||
self.pannedSelectionView.layer.contents = selectionImage.cgImage
|
||||
}
|
||||
|
||||
fileprivate func cancelDrawing() {
|
||||
|
|
@ -520,15 +545,24 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, TGPhotoDraw
|
|||
}
|
||||
|
||||
fileprivate func finishDrawing() {
|
||||
self.commit()
|
||||
|
||||
self.redoElements.removeAll()
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
self.elements.append(uncommitedElement)
|
||||
self.uncommitedElement = nil
|
||||
let complete: (Bool) -> Void = { synchronous in
|
||||
self.commit(interactive: true, synchronous: synchronous)
|
||||
|
||||
self.redoElements.removeAll()
|
||||
if let uncommitedElement = self.uncommitedElement {
|
||||
self.elements.append(uncommitedElement)
|
||||
self.uncommitedElement = nil
|
||||
}
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
if let uncommitedElement = self.uncommitedElement as? PenTool, uncommitedElement.arrow {
|
||||
uncommitedElement.finishArrow({
|
||||
complete(true)
|
||||
})
|
||||
} else {
|
||||
complete(false)
|
||||
}
|
||||
|
||||
self.updateInternalState()
|
||||
}
|
||||
|
||||
weak var entitiesView: DrawingEntitiesView?
|
||||
|
|
|
|||
|
|
@ -91,6 +91,32 @@ final class PenTool: DrawingElement {
|
|||
self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||
}
|
||||
|
||||
func animateArrowPaths(leftArrowPath: UIBezierPath, rightArrowPath: UIBezierPath, lineWidth: CGFloat, completion: @escaping () -> Void) {
|
||||
let leftArrowShape = CAShapeLayer()
|
||||
leftArrowShape.path = leftArrowPath.cgPath
|
||||
leftArrowShape.lineWidth = lineWidth
|
||||
leftArrowShape.strokeColor = self.color?.cgColor
|
||||
leftArrowShape.lineCap = .round
|
||||
leftArrowShape.frame = self.bounds
|
||||
self.addSublayer(leftArrowShape)
|
||||
|
||||
let rightArrowShape = CAShapeLayer()
|
||||
rightArrowShape.path = rightArrowPath.cgPath
|
||||
rightArrowShape.lineWidth = lineWidth
|
||||
rightArrowShape.strokeColor = self.color?.cgColor
|
||||
rightArrowShape.lineCap = .round
|
||||
rightArrowShape.frame = self.bounds
|
||||
self.addSublayer(rightArrowShape)
|
||||
|
||||
leftArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
||||
rightArrowShape.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "strokeEnd", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, completion: { [weak leftArrowShape, weak rightArrowShape] _ in
|
||||
completion()
|
||||
|
||||
leftArrowShape?.removeFromSuperlayer()
|
||||
rightArrowShape?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
|
||||
override func draw(in ctx: CGContext) {
|
||||
guard let color = self.color else {
|
||||
return
|
||||
|
|
@ -122,10 +148,17 @@ final class PenTool: DrawingElement {
|
|||
|
||||
var didSetupArrow = false
|
||||
let renderLineWidth: CGFloat
|
||||
let renderArrowLength: CGFloat
|
||||
let renderArrowLineWidth: CGFloat
|
||||
|
||||
var bezierPaths: [UIBezierPath] = []
|
||||
var tempBezierPath: UIBezierPath?
|
||||
|
||||
var arrowLeftPath: UIBezierPath?
|
||||
var arrowLeftPoint: CGPoint?
|
||||
var arrowRightPath: UIBezierPath?
|
||||
var arrowRightPoint: CGPoint?
|
||||
|
||||
var translation = CGPoint()
|
||||
|
||||
private var currentRenderLayer: DrawingRenderLayer?
|
||||
|
|
@ -138,8 +171,16 @@ final class PenTool: DrawingElement {
|
|||
|
||||
var points: [Polyline.Point] {
|
||||
var points: [Polyline.Point] = []
|
||||
var lastPoint: Polyline.Point?
|
||||
for point in self._points {
|
||||
points.append(point.offsetBy(self.translation))
|
||||
lastPoint = point
|
||||
}
|
||||
if let arrowLeftPoint, let lastPoint {
|
||||
points.append(lastPoint.withLocation(arrowLeftPoint.offsetBy(self.translation)))
|
||||
}
|
||||
if let arrowRightPoint, let lastPoint {
|
||||
points.append(lastPoint.withLocation(arrowRightPoint.offsetBy(self.translation)))
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
|
@ -148,9 +189,15 @@ final class PenTool: DrawingElement {
|
|||
private var nextPointIndex: Int = 0
|
||||
private var drawPoints = [PointWeighted](repeating: PointWeighted.zero, count: 4)
|
||||
|
||||
private var arrowParams: (CGPoint, CGFloat)?
|
||||
|
||||
func containsPoint(_ point: CGPoint) -> Bool {
|
||||
for path in self.bezierPaths {
|
||||
if path.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
}
|
||||
|
||||
func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
|
|
@ -178,12 +225,20 @@ final class PenTool: DrawingElement {
|
|||
let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
|
||||
self.renderLineWidth = lineWidth
|
||||
self.renderArrowLength = lineWidth * 7.0
|
||||
self.renderArrowLineWidth = lineWidth * 2.0
|
||||
|
||||
// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth)
|
||||
// if arrow {
|
||||
// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
// }
|
||||
self.path = Polyline(points: [])
|
||||
}
|
||||
|
||||
func finishArrow(_ completion: @escaping () -> Void) {
|
||||
if let arrowLeftPath, let arrowRightPath {
|
||||
(self.currentRenderLayer as? RenderLayer)?.animateArrowPaths(leftArrowPath: arrowLeftPath, rightArrowPath: arrowRightPath, lineWidth: self.renderArrowLineWidth, completion: {
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
|
|
@ -211,6 +266,7 @@ final class PenTool: DrawingElement {
|
|||
}
|
||||
|
||||
self._points.append(point)
|
||||
self.path?.points.append(point)
|
||||
|
||||
switch state {
|
||||
case .began:
|
||||
|
|
@ -222,6 +278,41 @@ final class PenTool: DrawingElement {
|
|||
}
|
||||
case .ended:
|
||||
self.updateWithLocation(point.location, ended: true)
|
||||
|
||||
if self.arrow {
|
||||
let points = self.path?.points ?? []
|
||||
var direction: CGFloat?
|
||||
|
||||
|
||||
let p2 = points[points.count - 1].location
|
||||
for i in 1 ..< min(points.count - 2, 12) {
|
||||
let p1 = points[points.count - 1 - i].location
|
||||
if p1.distance(to: p2) > renderArrowLength * 0.5 {
|
||||
direction = p2.angle(to: p1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let point = points.last?.location, let direction {
|
||||
self.arrowParams = (point, direction)
|
||||
|
||||
let arrowLeftPath = UIBezierPath()
|
||||
arrowLeftPath.move(to: point)
|
||||
let leftPoint = point.pointAt(distance: self.renderArrowLength, angle: direction - 0.45)
|
||||
arrowLeftPath.addLine(to: leftPoint)
|
||||
|
||||
let arrowRightPath = UIBezierPath()
|
||||
arrowRightPath.move(to: point)
|
||||
let rightPoint = point.pointAt(distance: self.renderArrowLength, angle: direction + 0.45)
|
||||
arrowRightPath.addLine(to: rightPoint)
|
||||
|
||||
self.arrowLeftPath = arrowLeftPath
|
||||
self.arrowLeftPoint = leftPoint
|
||||
|
||||
self.arrowRightPath = arrowRightPath
|
||||
self.arrowRightPoint = rightPoint
|
||||
}
|
||||
}
|
||||
case .cancelled:
|
||||
break
|
||||
}
|
||||
|
|
@ -229,41 +320,6 @@ final class PenTool: DrawingElement {
|
|||
if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
currentRenderLayer.draw(paths: self.bezierPaths, tempPath: self.tempBezierPath, color: self.color.toUIColor(), rect: CGRect(origin: .zero, size: self.drawingSize))
|
||||
}
|
||||
|
||||
// self.path = line
|
||||
//
|
||||
// let rect = self.renderLine.draw(at: point)
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.draw(line: self.renderLine, rect: rect)
|
||||
// }
|
||||
// self.path = bezierPath
|
||||
|
||||
// if self.arrow && polyline.isComplete, polyline.points.count > 2 {
|
||||
// let lastPoint = lastPosition
|
||||
// var secondPoint = polyline.points[polyline.points.count - 2]
|
||||
// if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
|
||||
// secondPoint = polyline.points[polyline.points.count - 3]
|
||||
// }
|
||||
// let angle = lastPoint.angle(to: secondPoint.location)
|
||||
// let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
|
||||
// let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
|
||||
//
|
||||
// let arrowPath = UIBezierPath()
|
||||
// arrowPath.move(to: point2)
|
||||
// arrowPath.addLine(to: lastPoint)
|
||||
// arrowPath.addLine(to: point1)
|
||||
// let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
//
|
||||
// combinedPath.usesEvenOddFillRule = false
|
||||
// combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
|
||||
// }
|
||||
|
||||
// let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
// self.renderPath = cgPath
|
||||
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.updatePath(cgPath)
|
||||
// }
|
||||
}
|
||||
|
||||
private let minDistance: CGFloat = 2
|
||||
|
|
@ -289,7 +345,6 @@ final class PenTool: DrawingElement {
|
|||
self.tempBezierPath = newBezier
|
||||
}
|
||||
|
||||
|
||||
private var isFirstPoint: Bool {
|
||||
return nextPointIndex == 0
|
||||
}
|
||||
|
|
@ -401,6 +456,18 @@ final class PenTool: DrawingElement {
|
|||
context.fillPath()
|
||||
}
|
||||
|
||||
if let arrowLeftPath, let arrowRightPath {
|
||||
context.setStrokeColor(self.color.toCGColor())
|
||||
context.setLineWidth(self.renderArrowLineWidth)
|
||||
context.setLineCap(.round)
|
||||
|
||||
context.addPath(arrowLeftPath.cgPath)
|
||||
context.strokePath()
|
||||
|
||||
context.addPath(arrowRightPath.cgPath)
|
||||
context.strokePath()
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
|
@ -503,431 +570,3 @@ extension UIBezierPath {
|
|||
return (line.normalLine(from: pointA), line.normalLine(from: pointB))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
//final class PenTool: DrawingElement {
|
||||
// class RenderLayer: SimpleLayer, DrawingRenderLayer {
|
||||
// func setup(size: CGSize) {
|
||||
// self.shouldRasterize = true
|
||||
// self.contentsScale = 1.0
|
||||
//
|
||||
// let bounds = CGRect(origin: .zero, size: size)
|
||||
// self.frame = bounds
|
||||
// }
|
||||
//
|
||||
// private var line: StrokeLine?
|
||||
// fileprivate func draw(line: StrokeLine, rect: CGRect) {
|
||||
// self.line = line
|
||||
// self.setNeedsDisplay(rect.insetBy(dx: -50.0, dy: -50.0))
|
||||
// }
|
||||
//
|
||||
// override func draw(in ctx: CGContext) {
|
||||
// self.line?.drawInContext(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let uuid = UUID()
|
||||
//
|
||||
// let drawingSize: CGSize
|
||||
// let color: DrawingColor
|
||||
// let lineWidth: CGFloat
|
||||
// let arrow: Bool
|
||||
//
|
||||
// var path: Polyline?
|
||||
// var boundingBox: CGRect?
|
||||
//
|
||||
// private var renderLine: StrokeLine
|
||||
// var didSetupArrow = false
|
||||
// private var renderLineArrow1: StrokeLine?
|
||||
// private var renderLineArrow2: StrokeLine?
|
||||
// let renderLineWidth: CGFloat
|
||||
//
|
||||
// var translation = CGPoint()
|
||||
//
|
||||
// private var currentRenderLayer: DrawingRenderLayer?
|
||||
//
|
||||
// var bounds: CGRect {
|
||||
// return self.path?.bounds.offsetBy(dx: self.translation.x, dy: self.translation.y) ?? .zero
|
||||
// }
|
||||
//
|
||||
// var points: [Polyline.Point] {
|
||||
// guard let linePath = self.path else {
|
||||
// return []
|
||||
// }
|
||||
// var points: [Polyline.Point] = []
|
||||
// for point in linePath.points {
|
||||
// points.append(point.offsetBy(self.translation))
|
||||
// }
|
||||
// return points
|
||||
// }
|
||||
//
|
||||
// func containsPoint(_ point: CGPoint) -> Bool {
|
||||
// return false
|
||||
// // return self.renderPath?.contains(point.offsetBy(CGPoint(x: -self.translation.x, y: -self.translation.y))) ?? false
|
||||
// }
|
||||
//
|
||||
// func hasPointsInsidePath(_ path: UIBezierPath) -> Bool {
|
||||
// if let linePath = self.path {
|
||||
// let pathBoundingBox = path.bounds
|
||||
// if self.bounds.intersects(pathBoundingBox) {
|
||||
// for point in linePath.points {
|
||||
// if path.contains(point.location.offsetBy(self.translation)) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// required init(drawingSize: CGSize, color: DrawingColor, lineWidth: CGFloat, arrow: Bool) {
|
||||
// self.drawingSize = drawingSize
|
||||
// self.color = color
|
||||
// self.lineWidth = lineWidth
|
||||
// self.arrow = arrow
|
||||
//
|
||||
// let minLineWidth = max(1.0, min(drawingSize.width, drawingSize.height) * 0.003)
|
||||
// let maxLineWidth = max(10.0, min(drawingSize.width, drawingSize.height) * 0.09)
|
||||
// let lineWidth = minLineWidth + (maxLineWidth - minLineWidth) * lineWidth
|
||||
//
|
||||
// self.renderLineWidth = lineWidth
|
||||
//
|
||||
// self.renderLine = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth)
|
||||
// if arrow {
|
||||
// self.renderLineArrow1 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
// self.renderLineArrow2 = StrokeLine(color: color.toUIColor(), minLineWidth: minLineWidth, lineWidth: lineWidth * 0.8)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func setupRenderLayer() -> DrawingRenderLayer? {
|
||||
// let layer = RenderLayer()
|
||||
// layer.setup(size: self.drawingSize)
|
||||
// self.currentRenderLayer = layer
|
||||
// return layer
|
||||
// }
|
||||
//
|
||||
// func updatePath(_ path: DrawingGesturePipeline.DrawingResult, state: DrawingGesturePipeline.DrawingGestureState) {
|
||||
// guard case let .polyline(line) = path, let point = line.points.last else {
|
||||
// return
|
||||
// }
|
||||
// self.path = line
|
||||
//
|
||||
// let rect = self.renderLine.draw(at: point)
|
||||
// if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// currentRenderLayer.draw(line: self.renderLine, rect: rect)
|
||||
// }
|
||||
// // self.path = bezierPath
|
||||
//
|
||||
// // if self.arrow && polyline.isComplete, polyline.points.count > 2 {
|
||||
// // let lastPoint = lastPosition
|
||||
// // var secondPoint = polyline.points[polyline.points.count - 2]
|
||||
// // if secondPoint.location.distance(to: lastPoint) < self.renderArrowLineWidth {
|
||||
// // secondPoint = polyline.points[polyline.points.count - 3]
|
||||
// // }
|
||||
// // let angle = lastPoint.angle(to: secondPoint.location)
|
||||
// // let point1 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle - CGFloat.pi * 0.15)
|
||||
// // let point2 = lastPoint.pointAt(distance: self.renderArrowLength, angle: angle + CGFloat.pi * 0.15)
|
||||
// //
|
||||
// // let arrowPath = UIBezierPath()
|
||||
// // arrowPath.move(to: point2)
|
||||
// // arrowPath.addLine(to: lastPoint)
|
||||
// // arrowPath.addLine(to: point1)
|
||||
// // let arrowThickPath = arrowPath.cgPath.copy(strokingWithWidth: self.renderArrowLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
// //
|
||||
// // combinedPath.usesEvenOddFillRule = false
|
||||
// // combinedPath.append(UIBezierPath(cgPath: arrowThickPath))
|
||||
// // }
|
||||
//
|
||||
// // let cgPath = bezierPath.path.cgPath.copy(strokingWithWidth: self.renderLineWidth, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
|
||||
// // self.renderPath = cgPath
|
||||
//
|
||||
// // if let currentRenderLayer = self.currentRenderLayer as? RenderLayer {
|
||||
// // currentRenderLayer.updatePath(cgPath)
|
||||
// // }
|
||||
// }
|
||||
//
|
||||
// func draw(in context: CGContext, size: CGSize) {
|
||||
// context.saveGState()
|
||||
//
|
||||
// context.translateBy(x: self.translation.x, y: self.translation.y)
|
||||
//
|
||||
// context.setShouldAntialias(true)
|
||||
//
|
||||
// if self.arrow, let path = self.path, let lastPoint = path.points.last {
|
||||
// var lastPointWithVelocity: Polyline.Point?
|
||||
// for point in path.points.reversed() {
|
||||
// if point.velocity > 0.0 {
|
||||
// lastPointWithVelocity = point
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if !self.didSetupArrow, let lastPointWithVelocity = lastPointWithVelocity {
|
||||
// let w = self.renderLineWidth
|
||||
// var dist: CGFloat = 18.0 * sqrt(w)
|
||||
// let spread: CGFloat = .pi * max(0.05, 0.03 * sqrt(w))
|
||||
//
|
||||
// let suffix = path.points.suffix(100).reversed()
|
||||
//
|
||||
// var p0 = suffix.first!
|
||||
//
|
||||
// var p2 = suffix.last!
|
||||
// var d: CGFloat = 0
|
||||
// for p in suffix {
|
||||
// d += hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
|
||||
// if d >= dist {
|
||||
// p2 = p
|
||||
// break
|
||||
// }
|
||||
// p0 = p
|
||||
// }
|
||||
//
|
||||
// p0 = suffix.first!
|
||||
// dist = min(dist, hypot(p0.location.x - p2.location.x, p0.location.y - p2.location.y))
|
||||
//
|
||||
// var i = 0
|
||||
// for spread in [-spread, spread] {
|
||||
// var points: [CGPoint] = []
|
||||
// points.append(lastPoint.location)
|
||||
//
|
||||
// p0 = suffix.first!
|
||||
// var prev = p0.location
|
||||
// d = 0
|
||||
// for p in suffix {
|
||||
// let d1 = hypot(p0.location.x - p.location.x, p0.location.y - p.location.y)
|
||||
// d += d1
|
||||
// if d >= dist {
|
||||
// break
|
||||
// }
|
||||
// let d2 = d1 / cos(spread)
|
||||
// let angle = atan2(p.location.y - p0.location.y, p.location.x - p0.location.x)
|
||||
// let cur = CGPoint(x: prev.x + d2 * cos(angle + spread), y: prev.y + d2 * sin(angle + spread))
|
||||
//
|
||||
// points.append(
|
||||
// cur
|
||||
// )
|
||||
//
|
||||
// p0 = p
|
||||
// prev = cur
|
||||
// }
|
||||
//
|
||||
// for point in points {
|
||||
// if i == 0 {
|
||||
// let _ = self.renderLineArrow1?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
|
||||
// } else if i == 1 {
|
||||
// let _ = self.renderLineArrow2?.draw(at: Polyline.Point(location: point, force: 0.0, altitudeAngle: 0.0, azimuth: 0.0, velocity: lastPointWithVelocity.velocity, touchPoint: lastPointWithVelocity.touchPoint))
|
||||
// }
|
||||
// }
|
||||
// i += 1
|
||||
// }
|
||||
// self.didSetupArrow = true
|
||||
// }
|
||||
// self.renderLineArrow1?.drawInContext(context)
|
||||
// self.renderLineArrow2?.drawInContext(context)
|
||||
// }
|
||||
//
|
||||
// self.renderLine.drawInContext(context)
|
||||
//
|
||||
// context.restoreGState()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//private class StrokeLine {
|
||||
// struct Segment {
|
||||
// let a: CGPoint
|
||||
// let b: CGPoint
|
||||
// let c: CGPoint
|
||||
// let d: CGPoint
|
||||
// let abWidth: CGFloat
|
||||
// let cdWidth: CGFloat
|
||||
// }
|
||||
//
|
||||
// struct Point {
|
||||
// let position: CGPoint
|
||||
// let width: CGFloat
|
||||
//
|
||||
// init(position: CGPoint, width: CGFloat) {
|
||||
// self.position = position
|
||||
// self.width = width
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private(set) var points: [Point] = []
|
||||
// private var smoothPoints: [Point] = []
|
||||
// private var segments: [Segment] = []
|
||||
// private var lastWidth: CGFloat?
|
||||
//
|
||||
// private let minLineWidth: CGFloat
|
||||
// let lineWidth: CGFloat
|
||||
//
|
||||
// let color: UIColor
|
||||
//
|
||||
// init(color: UIColor, minLineWidth: CGFloat, lineWidth: CGFloat) {
|
||||
// self.color = color
|
||||
// self.minLineWidth = minLineWidth
|
||||
// self.lineWidth = lineWidth
|
||||
// }
|
||||
//
|
||||
// func draw(at point: Polyline.Point) -> CGRect {
|
||||
// let width = extractLineWidth(from: point.velocity)
|
||||
// self.lastWidth = width
|
||||
//
|
||||
// let point = Point(position: point.location, width: width)
|
||||
// return appendPoint(point)
|
||||
// }
|
||||
//
|
||||
// func drawInContext(_ context: CGContext) {
|
||||
// self.drawSegments(self.segments, inContext: context)
|
||||
// }
|
||||
//
|
||||
// func extractLineWidth(from velocity: CGFloat) -> CGFloat {
|
||||
// let minValue = self.minLineWidth
|
||||
// let maxValue = self.lineWidth
|
||||
//
|
||||
// var size = max(minValue, min(maxValue + 1 - (velocity / 150), maxValue))
|
||||
// if let lastWidth = self.lastWidth {
|
||||
// size = size * 0.2 + lastWidth * 0.8
|
||||
// }
|
||||
// return size
|
||||
// }
|
||||
//
|
||||
// func appendPoint(_ point: Point) -> CGRect {
|
||||
// self.points.append(point)
|
||||
//
|
||||
// guard self.points.count > 2 else { return .null }
|
||||
//
|
||||
// let index = self.points.count - 1
|
||||
// let point0 = self.points[index - 2]
|
||||
// let point1 = self.points[index - 1]
|
||||
// let point2 = self.points[index]
|
||||
//
|
||||
// let newSmoothPoints = smoothPoints(
|
||||
// fromPoint0: point0,
|
||||
// point1: point1,
|
||||
// point2: point2
|
||||
// )
|
||||
//
|
||||
// let lastOldSmoothPoint = smoothPoints.last
|
||||
// smoothPoints.append(contentsOf: newSmoothPoints)
|
||||
//
|
||||
// guard smoothPoints.count > 1 else { return .null }
|
||||
//
|
||||
// let newSegments: ([Segment], CGRect) = {
|
||||
// guard let lastOldSmoothPoint = lastOldSmoothPoint else {
|
||||
// return segments(fromSmoothPoints: newSmoothPoints)
|
||||
// }
|
||||
// return segments(fromSmoothPoints: [lastOldSmoothPoint] + newSmoothPoints)
|
||||
// }()
|
||||
// segments.append(contentsOf: newSegments.0)
|
||||
//
|
||||
// return newSegments.1
|
||||
// }
|
||||
//
|
||||
// func smoothPoints(fromPoint0 point0: Point, point1: Point, point2: Point) -> [Point] {
|
||||
// var smoothPoints = [Point]()
|
||||
//
|
||||
// let midPoint1 = (point0.position + point1.position) * 0.5
|
||||
// let midPoint2 = (point1.position + point2.position) * 0.5
|
||||
//
|
||||
// let segmentDistance = 2.0
|
||||
// let distance = midPoint1.distance(to: midPoint2)
|
||||
// let numberOfSegments = min(128, max(floor(distance/segmentDistance), 32))
|
||||
//
|
||||
// let step = 1.0 / numberOfSegments
|
||||
// for t in stride(from: 0, to: 1, by: step) {
|
||||
// let position = midPoint1 * pow(1 - t, 2) + point1.position * 2 * (1 - t) * t + midPoint2 * t * t
|
||||
// let size = pow(1 - t, 2) * ((point0.width + point1.width) * 0.5) + 2 * (1 - t) * t * point1.width + t * t * ((point1.width + point2.width) * 0.5)
|
||||
// let point = Point(position: position, width: size)
|
||||
// smoothPoints.append(point)
|
||||
// }
|
||||
//
|
||||
// let finalPoint = Point(position: midPoint2, width: (point1.width + point2.width) * 0.5)
|
||||
// smoothPoints.append(finalPoint)
|
||||
//
|
||||
// return smoothPoints
|
||||
// }
|
||||
//
|
||||
// func segments(fromSmoothPoints smoothPoints: [Point]) -> ([Segment], CGRect) {
|
||||
// var segments = [Segment]()
|
||||
// var updateRect = CGRect.null
|
||||
// for i in 1 ..< smoothPoints.count {
|
||||
// let previousPoint = smoothPoints[i - 1].position
|
||||
// let previousWidth = smoothPoints[i - 1].width
|
||||
// let currentPoint = smoothPoints[i].position
|
||||
// let currentWidth = smoothPoints[i].width
|
||||
// let direction = currentPoint - previousPoint
|
||||
//
|
||||
// guard !currentPoint.isEqual(to: previousPoint, epsilon: 0.0001) else {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// var perpendicular = CGPoint(x: -direction.y, y: direction.x)
|
||||
// let length = perpendicular.length
|
||||
// if length > 0.0 {
|
||||
// perpendicular = perpendicular / length
|
||||
// }
|
||||
//
|
||||
// let a = previousPoint + perpendicular * previousWidth / 2
|
||||
// let b = previousPoint - perpendicular * previousWidth / 2
|
||||
// let c = currentPoint + perpendicular * currentWidth / 2
|
||||
// let d = currentPoint - perpendicular * currentWidth / 2
|
||||
//
|
||||
// let ab: CGPoint = {
|
||||
// let center = (b + a)/2
|
||||
// let radius = center - b
|
||||
// return .init(x: center.x - radius.y, y: center.y + radius.x)
|
||||
// }()
|
||||
// let cd: CGPoint = {
|
||||
// let center = (c + d)/2
|
||||
// let radius = center - c
|
||||
// return .init(x: center.x + radius.y, y: center.y - radius.x)
|
||||
// }()
|
||||
//
|
||||
// let minX = min(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
||||
// let minY = min(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
||||
// let maxX = max(a.x, b.x, c.x, d.x, ab.x, cd.x)
|
||||
// let maxY = max(a.y, b.y, c.y, d.y, ab.y, cd.y)
|
||||
//
|
||||
// updateRect = updateRect.union(CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY))
|
||||
//
|
||||
// segments.append(Segment(a: a, b: b, c: c, d: d, abWidth: previousWidth, cdWidth: currentWidth))
|
||||
// }
|
||||
// return (segments, updateRect)
|
||||
// }
|
||||
//
|
||||
// func drawSegments(_ segments: [Segment], inContext context: CGContext) {
|
||||
// for segment in segments {
|
||||
// context.beginPath()
|
||||
//
|
||||
// context.setStrokeColor(color.cgColor)
|
||||
// context.setFillColor(color.cgColor)
|
||||
//
|
||||
// context.move(to: segment.b)
|
||||
//
|
||||
// let abStartAngle = atan2(segment.b.y - segment.a.y, segment.b.x - segment.a.x)
|
||||
// context.addArc(
|
||||
// center: (segment.a + segment.b)/2,
|
||||
// radius: segment.abWidth/2,
|
||||
// startAngle: abStartAngle,
|
||||
// endAngle: abStartAngle + .pi,
|
||||
// clockwise: true
|
||||
// )
|
||||
// context.addLine(to: segment.c)
|
||||
//
|
||||
// let cdStartAngle = atan2(segment.c.y - segment.d.y, segment.c.x - segment.d.x)
|
||||
// context.addArc(
|
||||
// center: (segment.c + segment.d)/2,
|
||||
// radius: segment.cdWidth/2,
|
||||
// startAngle: cdStartAngle,
|
||||
// endAngle: cdStartAngle + .pi,
|
||||
// clockwise: true
|
||||
// )
|
||||
// context.closePath()
|
||||
//
|
||||
// context.fillPath()
|
||||
// context.strokePath()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
|
|
|
|||
|
|
@ -11,29 +11,49 @@ import ComponentFlow
|
|||
import ViewControllerComponent
|
||||
import EntityKeyboard
|
||||
import PagerComponent
|
||||
import FeaturedStickersScreen
|
||||
|
||||
struct InputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent?
|
||||
var masks: EmojiPagerContentComponent?
|
||||
|
||||
init(
|
||||
emoji: EmojiPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent?,
|
||||
masks: EmojiPagerContentComponent?
|
||||
) {
|
||||
self.emoji = emoji
|
||||
self.stickers = stickers
|
||||
self.masks = masks
|
||||
}
|
||||
}
|
||||
|
||||
private final class StickerSelectionComponent: Component {
|
||||
public typealias EnvironmentType = Empty
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let stickerContent: EmojiPagerContentComponent
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let deviceMetrics: DeviceMetrics
|
||||
let bottomInset: CGFloat
|
||||
let content: InputData
|
||||
let backgroundColor: UIColor
|
||||
let separatorColor: UIColor
|
||||
|
||||
public init(
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
stickerContent: EmojiPagerContentComponent,
|
||||
bottomInset: CGFloat,
|
||||
content: InputData,
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.stickerContent = stickerContent
|
||||
self.bottomInset = bottomInset
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
}
|
||||
|
|
@ -48,7 +68,10 @@ private final class StickerSelectionComponent: Component {
|
|||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
if lhs.stickerContent != rhs.stickerContent {
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
|
|
@ -96,7 +119,7 @@ private final class StickerSelectionComponent: Component {
|
|||
self.backgroundColor = component.backgroundColor
|
||||
let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85)
|
||||
self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate)
|
||||
self.panelSeparatorView.backgroundColor = component.separatorColor
|
||||
self.panelSeparatorView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
|
@ -109,15 +132,18 @@ private final class StickerSelectionComponent: Component {
|
|||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
isContentInFocus: true,
|
||||
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: component.bottomInset, right: 0.0),
|
||||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: nil,
|
||||
stickerContent: component.stickerContent,
|
||||
emojiContent: component.content.emoji,
|
||||
stickerContent: component.content.stickers,
|
||||
maskContent: component.content.masks,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: false,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
externalBottomPanelContainer: nil,
|
||||
displayTopPanelBackground: true,
|
||||
topPanelExtensionUpdated: { _, _ in },
|
||||
hideInputUpdated: { _, _, _ in },
|
||||
hideTopPanelUpdated: { _, _ in },
|
||||
|
|
@ -128,7 +154,7 @@ private final class StickerSelectionComponent: Component {
|
|||
deviceMetrics: component.deviceMetrics,
|
||||
hiddenInputHeight: 0.0,
|
||||
inputHeight: 0.0,
|
||||
displayBottomPanel: false,
|
||||
displayBottomPanel: true,
|
||||
isExpanded: true
|
||||
)),
|
||||
environment: {},
|
||||
|
|
@ -182,8 +208,8 @@ public class StickerPickerScreen: ViewController {
|
|||
let scrollView: UIScrollView
|
||||
let hostView: ComponentHostView<Empty>
|
||||
|
||||
private var stickerContent: EmojiPagerContentComponent?
|
||||
private let stickerContentDisposable = MetaDisposable()
|
||||
private var content: InputData?
|
||||
private let contentDisposable = MetaDisposable()
|
||||
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
|
||||
|
||||
private(set) var isExpanded = false
|
||||
|
|
@ -224,31 +250,63 @@ public class StickerPickerScreen: ViewController {
|
|||
self.containerView.addSubview(self.scrollView)
|
||||
self.scrollView.addSubview(self.hostView)
|
||||
|
||||
self.stickerContentDisposable.set((
|
||||
EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] content in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateContent(content)
|
||||
}
|
||||
})
|
||||
let emojiItems = EmojiPagerContentComponent.emojiInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
isStandalone: false,
|
||||
isStatusSelection: false,
|
||||
isReactionSelection: false,
|
||||
isEmojiSelection: true,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: true,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false
|
||||
)
|
||||
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
|
||||
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasTrending: true
|
||||
)
|
||||
|
||||
let maskItems = EmojiPagerContentComponent.stickerInputData(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks],
|
||||
stickerOrderedItemListCollectionIds: [],
|
||||
chatPeerId: context.account.peerId,
|
||||
hasSearch: false,
|
||||
hasTrending: false
|
||||
)
|
||||
|
||||
let signal = combineLatest(queue: .mainQueue(),
|
||||
emojiItems,
|
||||
stickerItems,
|
||||
maskItems
|
||||
)
|
||||
self.contentDisposable.set(signal.start(next: { [weak self] emoji, stickers, masks in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateContent(InputData(emoji: emoji, stickers: stickers, masks: masks))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerContentDisposable.dispose()
|
||||
self.contentDisposable.dispose()
|
||||
}
|
||||
|
||||
|
||||
func updateContent(_ content: EmojiPagerContentComponent) {
|
||||
self.stickerContent = content
|
||||
content.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
func updateContent(_ content: InputData) {
|
||||
self.content = content
|
||||
|
||||
content.emoji.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let strongSelf = self, let file = item.itemFile else {
|
||||
return
|
||||
|
|
@ -256,30 +314,229 @@ public class StickerPickerScreen: ViewController {
|
|||
strongSelf.controller?.completion(file)
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
},
|
||||
deleteBackwards: {},
|
||||
openStickerSettings: {
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return
|
||||
// }
|
||||
// let controller = installedStickerPacksController(context: context, mode: .modal)
|
||||
// controller.navigationPresentation = .modal
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
},
|
||||
deleteBackwards: nil,
|
||||
openStickerSettings: nil,
|
||||
openFeatured: {
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen(
|
||||
// context: context,
|
||||
// highlightedPackId: nil,
|
||||
// sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return false
|
||||
// }
|
||||
// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
// }
|
||||
// ))
|
||||
},
|
||||
openSearch: {},
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
if isPremiumLocked {
|
||||
// let controller = PremiumIntroScreen(context: context, source: .stickers)
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { views in
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return
|
||||
}
|
||||
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredStickerPack.info.id == collectionId {
|
||||
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clearGroup: { [weak self] groupId in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
if groupId == AnyHashable("popular") {
|
||||
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
||||
var items: [ActionSheetItem] = []
|
||||
let context = controller.context
|
||||
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
|
||||
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
||||
}
|
||||
},
|
||||
pushController: { c in },
|
||||
presentController: { c in },
|
||||
presentGlobalOverlayController: { c in },
|
||||
navigationController: { [weak self] in
|
||||
return self?.controller?.navigationController as? NavigationController
|
||||
},
|
||||
requestUpdate: { _ in },
|
||||
updateSearchQuery: { _, _ in },
|
||||
chatPeerId: nil,
|
||||
peekBehavior: nil,
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
useOpaqueTheme: false
|
||||
)
|
||||
|
||||
content.masks?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let strongSelf = self, let file = item.itemFile else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.completion(file)
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
},
|
||||
deleteBackwards: nil,
|
||||
openStickerSettings: nil,
|
||||
openFeatured: {
|
||||
},
|
||||
openSearch: {},
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
|
||||
if isPremiumLocked {
|
||||
// let controller = PremiumIntroScreen(context: context, source: .stickers)
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
|
||||
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { views in
|
||||
guard let view = views.views[viewKey] as? OrderedItemListView else {
|
||||
return
|
||||
}
|
||||
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
|
||||
if featuredStickerPack.info.id == collectionId {
|
||||
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clearGroup: { [weak self] groupId in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
if groupId == AnyHashable("popular") {
|
||||
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
|
||||
var items: [ActionSheetItem] = []
|
||||
let context = controller.context
|
||||
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
|
||||
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
|
||||
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
|
||||
}))
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
|
||||
}
|
||||
},
|
||||
pushController: { c in },
|
||||
presentController: { c in },
|
||||
presentGlobalOverlayController: { c in },
|
||||
navigationController: { [weak self] in
|
||||
return self?.controller?.navigationController as? NavigationController
|
||||
},
|
||||
requestUpdate: { _ in },
|
||||
updateSearchQuery: { _, _ in },
|
||||
chatPeerId: nil,
|
||||
peekBehavior: nil,
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
useOpaqueTheme: false
|
||||
)
|
||||
|
||||
content.stickers?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let strongSelf = self, let file = item.itemFile else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.completion(file)
|
||||
strongSelf.controller?.dismiss(animated: true)
|
||||
},
|
||||
deleteBackwards: nil,
|
||||
openStickerSettings: nil,
|
||||
openFeatured: { [weak self] in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?.push(
|
||||
FeaturedStickersScreen(
|
||||
context: controller.context,
|
||||
highlightedPackId: nil,
|
||||
sendSticker: { _, _, _ in
|
||||
// guard let controllerInteraction = controllerInteraction else {
|
||||
// return false
|
||||
// }
|
||||
return false
|
||||
// return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
openSearch: {},
|
||||
addGroupAction: { [weak self] groupId, isPremiumLocked in
|
||||
|
|
@ -477,6 +734,7 @@ public class StickerPickerScreen: ViewController {
|
|||
self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let clipFrame: CGRect
|
||||
let contentFrame: CGRect
|
||||
if layout.metrics.widthClass == .compact {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
|
||||
if isLandscape {
|
||||
|
|
@ -495,6 +753,7 @@ public class StickerPickerScreen: ViewController {
|
|||
|
||||
if isLandscape {
|
||||
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
contentFrame = clipFrame
|
||||
} else {
|
||||
let coveredByModalTransition: CGFloat = 0.0
|
||||
var containerTopInset: CGFloat = 10.0
|
||||
|
|
@ -510,6 +769,7 @@ public class StickerPickerScreen: ViewController {
|
|||
let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
|
||||
|
||||
clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height)
|
||||
contentFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height - topInset)
|
||||
}
|
||||
} else {
|
||||
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4)
|
||||
|
|
@ -521,12 +781,13 @@ public class StickerPickerScreen: ViewController {
|
|||
let minSide = min(layout.size.width, layout.size.height)
|
||||
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
|
||||
clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
|
||||
contentFrame = clipFrame
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.containerView, frame: clipFrame)
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil)
|
||||
|
||||
if let stickerContent = self.stickerContent {
|
||||
if let content = self.content {
|
||||
var stickersTransition: Transition = transition
|
||||
if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint {
|
||||
self.scheduledEmojiContentAnimationHint = nil
|
||||
|
|
@ -541,14 +802,15 @@ public class StickerPickerScreen: ViewController {
|
|||
theme: self.theme,
|
||||
strings: self.presentationData.strings,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
stickerContent: stickerContent,
|
||||
bottomInset: layout.intrinsicInsets.bottom,
|
||||
content: content,
|
||||
backgroundColor: self.theme.list.itemBlocksBackgroundColor,
|
||||
separatorColor: self.theme.list.blocksBackgroundColor
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
forceUpdate: true,
|
||||
containerSize: CGSize(width: clipFrame.size.width, height: clipFrame.height)
|
||||
containerSize: CGSize(width: contentFrame.size.width, height: contentFrame.height)
|
||||
)
|
||||
contentSize.height = max(layout.size.height - navigationHeight, contentSize.height)
|
||||
transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil)
|
||||
|
|
@ -816,10 +1078,6 @@ public class StickerPickerScreen: ViewController {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
override open func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
|
||||
self.displayNodeDidLoad()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import ComponentFlow
|
|||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import LottieAnimationComponent
|
||||
|
||||
enum DrawingTextStyle: Equatable {
|
||||
case regular
|
||||
|
|
@ -175,21 +176,29 @@ final class TextAlignmentComponent: Component {
|
|||
}
|
||||
|
||||
final class TextFontComponent: Component {
|
||||
let styleButton: AnyComponent<Empty>
|
||||
let alignmentButton: AnyComponent<Empty>
|
||||
|
||||
let values: [DrawingTextFont]
|
||||
let selectedValue: DrawingTextFont
|
||||
let updated: (DrawingTextFont) -> Void
|
||||
|
||||
init(values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) {
|
||||
init(styleButton: AnyComponent<Empty>, alignmentButton: AnyComponent<Empty>, values: [DrawingTextFont], selectedValue: DrawingTextFont, updated: @escaping (DrawingTextFont) -> Void) {
|
||||
self.styleButton = styleButton
|
||||
self.alignmentButton = alignmentButton
|
||||
self.values = values
|
||||
self.selectedValue = selectedValue
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
static func == (lhs: TextFontComponent, rhs: TextFontComponent) -> Bool {
|
||||
return lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue
|
||||
return lhs.styleButton == rhs.styleButton && lhs.alignmentButton == rhs.alignmentButton && lhs.values == rhs.values && lhs.selectedValue == rhs.selectedValue
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let styleButtonHost: ComponentView<Empty>
|
||||
private let alignmentButtonHost: ComponentView<Empty>
|
||||
|
||||
private var buttons: [DrawingTextFont: HighlightableButton] = [:]
|
||||
private let scrollView = UIScrollView()
|
||||
private let scrollMask = UIView()
|
||||
|
|
@ -207,6 +216,9 @@ final class TextFontComponent: Component {
|
|||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.decelerationRate = .fast
|
||||
|
||||
self.styleButtonHost = ComponentView()
|
||||
self.alignmentButtonHost = ComponentView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.mask = self.scrollMask
|
||||
|
|
@ -251,6 +263,36 @@ final class TextFontComponent: Component {
|
|||
|
||||
var contentWidth: CGFloat = 0.0
|
||||
|
||||
let styleSize = self.styleButtonHost.update(
|
||||
transition: transition,
|
||||
component: component.styleButton,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = self.styleButtonHost.view {
|
||||
if view.superview == nil {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: -7.0, y: -7.0), size: styleSize)
|
||||
}
|
||||
|
||||
contentWidth += 44.0
|
||||
|
||||
let alignmentSize = self.alignmentButtonHost.update(
|
||||
transition: transition,
|
||||
component: component.alignmentButton,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
if let view = self.alignmentButtonHost.view {
|
||||
if view.superview == nil {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
view.frame = CGRect(origin: CGPoint(x: contentWidth - 7.0, y: -7.0), size: alignmentSize)
|
||||
}
|
||||
|
||||
contentWidth += 32.0
|
||||
|
||||
for value in component.values {
|
||||
contentWidth += 12.0
|
||||
let button: HighlightableButton
|
||||
|
|
@ -319,6 +361,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
let style: DrawingTextStyle
|
||||
let alignment: DrawingTextAlignment
|
||||
let font: DrawingTextFont
|
||||
let isEmojiKeyboard: Bool
|
||||
|
||||
let presentColorPicker: () -> Void
|
||||
let presentFastColorPicker: (GenericComponentViewTag) -> Void
|
||||
|
|
@ -327,24 +370,28 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
let toggleStyle: () -> Void
|
||||
let toggleAlignment: () -> Void
|
||||
let updateFont: (DrawingTextFont) -> Void
|
||||
|
||||
let toggleKeyboard: (() -> Void)?
|
||||
|
||||
init(
|
||||
color: DrawingColor?,
|
||||
style: DrawingTextStyle,
|
||||
alignment: DrawingTextAlignment,
|
||||
font: DrawingTextFont,
|
||||
isEmojiKeyboard: Bool,
|
||||
presentColorPicker: @escaping () -> Void = {},
|
||||
presentFastColorPicker: @escaping (GenericComponentViewTag) -> Void = { _ in },
|
||||
updateFastColorPickerPan: @escaping (CGPoint) -> Void = { _ in },
|
||||
dismissFastColorPicker: @escaping () -> Void = {},
|
||||
toggleStyle: @escaping () -> Void,
|
||||
toggleAlignment: @escaping () -> Void,
|
||||
updateFont: @escaping (DrawingTextFont) -> Void
|
||||
updateFont: @escaping (DrawingTextFont) -> Void,
|
||||
toggleKeyboard: (() -> Void)?
|
||||
) {
|
||||
self.color = color
|
||||
self.style = style
|
||||
self.alignment = alignment
|
||||
self.font = font
|
||||
self.isEmojiKeyboard = isEmojiKeyboard
|
||||
self.presentColorPicker = presentColorPicker
|
||||
self.presentFastColorPicker = presentFastColorPicker
|
||||
self.updateFastColorPickerPan = updateFastColorPickerPan
|
||||
|
|
@ -352,6 +399,7 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
self.toggleStyle = toggleStyle
|
||||
self.toggleAlignment = toggleAlignment
|
||||
self.updateFont = updateFont
|
||||
self.toggleKeyboard = toggleKeyboard
|
||||
}
|
||||
|
||||
static func ==(lhs: TextSettingsComponent, rhs: TextSettingsComponent) -> Bool {
|
||||
|
|
@ -367,6 +415,9 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
if lhs.font != rhs.font {
|
||||
return false
|
||||
}
|
||||
if lhs.isEmojiKeyboard != rhs.isEmojiKeyboard {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -376,6 +427,8 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
case filled
|
||||
case semi
|
||||
case stroke
|
||||
case keyboard
|
||||
case emoji
|
||||
}
|
||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||
func image(_ key: ImageKey) -> UIImage {
|
||||
|
|
@ -392,6 +445,10 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
image = UIImage(bundleImageName: "Media Editor/TextSemi")!
|
||||
case .stroke:
|
||||
image = UIImage(bundleImageName: "Media Editor/TextStroke")!
|
||||
case .keyboard:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: .white)!
|
||||
case .emoji:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputEmojiIcon"), color: .white)!
|
||||
}
|
||||
cachedImages[key] = image
|
||||
return image
|
||||
|
|
@ -407,8 +464,9 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
let colorButton = Child(ColorSwatchComponent.self)
|
||||
let colorButtonTag = GenericComponentViewTag()
|
||||
|
||||
let alignmentButton = Child(Button.self)
|
||||
let styleButton = Child(Button.self)
|
||||
// let styleButton = Child(Button.self)
|
||||
// let alignmentButton = Child(Button.self)
|
||||
let keyboardButton = Child(Button.self)
|
||||
let font = Child(TextFontComponent.self)
|
||||
|
||||
return { context in
|
||||
|
|
@ -465,69 +523,71 @@ final class TextSettingsComponent: CombinedComponent {
|
|||
styleImage = state.image(.stroke)
|
||||
}
|
||||
|
||||
let styleButton = styleButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(
|
||||
image: styleImage
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleStyle()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(styleButton
|
||||
.position(CGPoint(x: offset + styleButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
.update(Transition.Update { _, view, transition in
|
||||
if let snapshot = view.snapshotView(afterScreenUpdates: false) {
|
||||
transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in
|
||||
snapshot?.removeFromSuperview()
|
||||
})
|
||||
snapshot.frame = view.frame
|
||||
transition.animateAlpha(view: view, from: 0.0, to: 1.0)
|
||||
view.superview?.addSubview(snapshot)
|
||||
}
|
||||
})
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
let alignmentButton = alignmentButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
TextAlignmentComponent(
|
||||
alignment: component.alignment
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleAlignment()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
availableSize: context.availableSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(alignmentButton
|
||||
.position(CGPoint(x: offset + alignmentButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
var fontAvailableWidth: CGFloat = context.availableSize.width
|
||||
if component.color != nil {
|
||||
fontAvailableWidth -= 88.0
|
||||
}
|
||||
|
||||
let font = font.update(
|
||||
component: TextFontComponent(
|
||||
styleButton: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
Image(
|
||||
image: styleImage
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleStyle()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
),
|
||||
alignmentButton: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
TextAlignmentComponent(
|
||||
alignment: component.alignment
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleAlignment()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))
|
||||
),
|
||||
values: DrawingTextFont.allCases,
|
||||
selectedValue: component.font,
|
||||
updated: { font in
|
||||
updateFont(font)
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - offset, height: 30.0),
|
||||
availableSize: CGSize(width: fontAvailableWidth, height: 30.0),
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(font
|
||||
.position(CGPoint(x: offset + font.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
offset += 44.0
|
||||
|
||||
if let toggleKeyboard = component.toggleKeyboard {
|
||||
let keyboardButton = keyboardButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(name: !component.isEmojiKeyboard ? "input_anim_smileToKey" : "input_anim_keyToSmile" , mode: .animateTransitionFromPrevious),
|
||||
colors: ["__allcolors__": UIColor.white],
|
||||
size: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
toggleKeyboard()
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||
availableSize: CGSize(width: 32.0, height: 32.0),
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(keyboardButton
|
||||
.position(CGPoint(x: context.availableSize.width - keyboardButton.size.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
)
|
||||
}
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
|
|
|
|||
39
submodules/FeaturedStickersScreen/BUILD
Normal file
39
submodules/FeaturedStickersScreen/BUILD
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "FeaturedStickersScreen",
|
||||
module_name = "FeaturedStickersScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/SearchBarNode:SearchBarNode",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ChatPresentationInterfaceState
|
||||
import TelegramPresentationData
|
||||
|
||||
open class ChatMediaInputPane: ASDisplayNode {
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var collectionListPanelOffset: CGFloat = 0.0
|
||||
var isEmpty: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
open func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
}
|
||||
}
|
||||
|
|
@ -12,15 +12,16 @@ import AccountContext
|
|||
import StickerPackPreviewUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import ChatControllerInteraction
|
||||
|
||||
final class TrendingPaneInteraction {
|
||||
let installPack: (ItemCollectionInfo) -> Void
|
||||
let openPack: (ItemCollectionInfo) -> Void
|
||||
let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
let openSearch: () -> Void
|
||||
let itemContext = StickerPaneSearchGlobalItemContext()
|
||||
public final class TrendingPaneInteraction {
|
||||
public let installPack: (ItemCollectionInfo) -> Void
|
||||
public let openPack: (ItemCollectionInfo) -> Void
|
||||
public let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
public let openSearch: () -> Void
|
||||
public let itemContext = StickerPaneSearchGlobalItemContext()
|
||||
|
||||
init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) {
|
||||
public init(installPack: @escaping (ItemCollectionInfo) -> Void, openPack: @escaping (ItemCollectionInfo) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, openSearch: @escaping () -> Void) {
|
||||
self.installPack = installPack
|
||||
self.openPack = openPack
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
|
|
@ -28,17 +29,17 @@ final class TrendingPaneInteraction {
|
|||
}
|
||||
}
|
||||
|
||||
final class TrendingPanePackEntry: Identifiable, Comparable {
|
||||
let index: Int
|
||||
let info: StickerPackCollectionInfo
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let topItems: [StickerPackItem]
|
||||
let installed: Bool
|
||||
let unread: Bool
|
||||
let topSeparator: Bool
|
||||
public final class TrendingPanePackEntry: Identifiable, Comparable {
|
||||
public let index: Int
|
||||
public let info: StickerPackCollectionInfo
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let topItems: [StickerPackItem]
|
||||
public let installed: Bool
|
||||
public let unread: Bool
|
||||
public let topSeparator: Bool
|
||||
|
||||
init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) {
|
||||
public init(index: Int, info: StickerPackCollectionInfo, theme: PresentationTheme, strings: PresentationStrings, topItems: [StickerPackItem], installed: Bool, unread: Bool, topSeparator: Bool) {
|
||||
self.index = index
|
||||
self.info = info
|
||||
self.theme = theme
|
||||
|
|
@ -49,11 +50,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable {
|
|||
self.topSeparator = topSeparator
|
||||
}
|
||||
|
||||
var stableId: ItemCollectionId {
|
||||
public var stableId: ItemCollectionId {
|
||||
return self.info.id
|
||||
}
|
||||
|
||||
static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
|
||||
public static func ==(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
|
|
@ -81,11 +82,11 @@ final class TrendingPanePackEntry: Identifiable, Comparable {
|
|||
return true
|
||||
}
|
||||
|
||||
static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
|
||||
public static func <(lhs: TrendingPanePackEntry, rhs: TrendingPanePackEntry) -> Bool {
|
||||
return lhs.index < rhs.index
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
|
||||
public func item(account: Account, interaction: TrendingPaneInteraction, grid: Bool) -> GridItem {
|
||||
let info = self.info
|
||||
return StickerPaneSearchGlobalItem(account: account, theme: self.theme, strings: self.strings, listAppearance: false, info: self.info, topItems: self.topItems, topSeparator: self.topSeparator, regularInsets: false, installed: self.installed, unread: self.unread, open: {
|
||||
interaction.openPack(info)
|
||||
|
|
@ -190,13 +191,13 @@ private func trendingPaneEntries(trendingEntries: [FeaturedStickerPackItem], ins
|
|||
return result
|
||||
}
|
||||
|
||||
final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
public final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
private let isPane: Bool
|
||||
|
||||
let gridNode: GridNode
|
||||
public let gridNode: GridNode
|
||||
|
||||
private var enqueuedTransitions: [TrendingPaneTransition] = []
|
||||
private var validLayout: (CGSize, CGFloat)?
|
||||
|
|
@ -206,15 +207,15 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
|
||||
private let _ready = Promise<Void>()
|
||||
private var didSetReady = false
|
||||
var ready: Signal<Void, NoError> {
|
||||
public var ready: Signal<Void, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
var scrollingInitiated: (() -> Void)?
|
||||
public var scrollingInitiated: (() -> Void)?
|
||||
|
||||
private let installDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
|
||||
public init(context: AccountContext, controllerInteraction: ChatControllerInteraction, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, isPane: Bool) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
|
|
@ -236,7 +237,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
self.installDisposable.dispose()
|
||||
}
|
||||
|
||||
func activate() {
|
||||
public func activate() {
|
||||
if self.isActivated {
|
||||
return
|
||||
}
|
||||
|
|
@ -369,7 +370,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
})
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
public override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
let hadValidLayout = self.validLayout != nil
|
||||
self.validLayout = (size, bottomInset)
|
||||
|
||||
|
|
@ -401,7 +402,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
}
|
||||
}
|
||||
|
||||
override func willEnterHierarchy() {
|
||||
public override func willEnterHierarchy() {
|
||||
super.willEnterHierarchy()
|
||||
|
||||
self.activate()
|
||||
|
|
@ -416,7 +417,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
}
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
|
||||
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
|
||||
let localPoint = self.view.convert(point, to: self.gridNode.view)
|
||||
var resultNode: StickerPaneSearchGlobalItemNode?
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
|
|
@ -430,7 +431,7 @@ final class ChatMediaInputTrendingPane: ChatMediaInputPane {
|
|||
return nil
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
public func updatePreviewing(animated: Bool) {
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
|
|
@ -17,6 +17,7 @@ import SearchBarNode
|
|||
import UndoUI
|
||||
import ContextUI
|
||||
import PremiumUI
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
private final class FeaturedInteraction {
|
||||
let installPack: (ItemCollectionInfo, Bool) -> Void
|
||||
|
|
@ -787,7 +788,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode {
|
|||
}
|
||||
}
|
||||
|
||||
final class FeaturedStickersScreen: ViewController {
|
||||
public final class FeaturedStickersScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
fileprivate let highlightedPackId: ItemCollectionId?
|
||||
private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
|
||||
|
|
@ -1492,3 +1493,17 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class StickerPaneSearchInteraction {
|
||||
public let open: (StickerPackCollectionInfo) -> Void
|
||||
public let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void
|
||||
public let sendSticker: (FileMediaReference, UIView, CGRect) -> Void
|
||||
public let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
|
||||
public init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
|
||||
self.open = open
|
||||
self.install = install
|
||||
self.sendSticker = sendSticker
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
}
|
||||
}
|
||||
|
|
@ -11,35 +11,35 @@ private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
|||
return generateTintedImage(image: templateLoupeIcon, color: color)
|
||||
}
|
||||
|
||||
enum PaneSearchBarType {
|
||||
public enum PaneSearchBarType {
|
||||
case stickers
|
||||
case gifs
|
||||
}
|
||||
|
||||
final class PaneSearchBarPlaceholderItem: GridItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let type: PaneSearchBarType
|
||||
let activate: () -> Void
|
||||
public final class PaneSearchBarPlaceholderItem: GridItem {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let type: PaneSearchBarType
|
||||
public let activate: () -> Void
|
||||
|
||||
let section: GridSection? = nil
|
||||
let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true)
|
||||
public let section: GridSection? = nil
|
||||
public let fillsRowWithHeight: (CGFloat, Bool)? = (56.0, true)
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType, activate: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.type = type
|
||||
self.activate = activate
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = PaneSearchBarPlaceholderNode()
|
||||
node.activate = self.activate
|
||||
node.setup(theme: self.theme, strings: self.strings, type: self.type)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
public func update(node: GridItemNode) {
|
||||
guard let node = node as? PaneSearchBarPlaceholderNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
|
|
@ -49,15 +49,15 @@ final class PaneSearchBarPlaceholderItem: GridItem {
|
|||
}
|
||||
}
|
||||
|
||||
final class PaneSearchBarPlaceholderNode: GridItemNode {
|
||||
public final class PaneSearchBarPlaceholderNode: GridItemNode {
|
||||
private var currentState: (PresentationTheme, PresentationStrings, PaneSearchBarType)?
|
||||
var activate: (() -> Void)?
|
||||
public var activate: (() -> Void)?
|
||||
|
||||
let backgroundNode: ASImageNode
|
||||
let labelNode: ImmediateTextNode
|
||||
let iconNode: ASImageNode
|
||||
public let backgroundNode: ASImageNode
|
||||
public let labelNode: ImmediateTextNode
|
||||
public let iconNode: ASImageNode
|
||||
|
||||
override init() {
|
||||
public override init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
|
|
@ -82,13 +82,13 @@ final class PaneSearchBarPlaceholderNode: GridItemNode {
|
|||
self.addSubnode(self.iconNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
|
||||
public func setup(theme: PresentationTheme, strings: PresentationStrings, type: PaneSearchBarType) {
|
||||
if self.currentState?.0 !== theme || self.currentState?.1 !== strings || self.currentState?.2 != type {
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
|
||||
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
|
||||
|
|
@ -105,7 +105,7 @@ final class PaneSearchBarPlaceholderNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let bounds = self.bounds
|
||||
|
|
@ -69,30 +69,34 @@ private final class StickerPaneSearchGlobalSectionNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
final class StickerPaneSearchGlobalItemContext {
|
||||
var canPlayMedia: Bool = false
|
||||
public final class StickerPaneSearchGlobalItemContext {
|
||||
public var canPlayMedia: Bool
|
||||
|
||||
public init(canPlayMedia: Bool = false) {
|
||||
self.canPlayMedia = canPlayMedia
|
||||
}
|
||||
}
|
||||
|
||||
final class StickerPaneSearchGlobalItem: GridItem {
|
||||
let account: Account
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let listAppearance: Bool
|
||||
let fillsRow: Bool
|
||||
let info: StickerPackCollectionInfo
|
||||
let topItems: [StickerPackItem]
|
||||
let topSeparator: Bool
|
||||
let regularInsets: Bool
|
||||
let installed: Bool
|
||||
let installing: Bool
|
||||
let unread: Bool
|
||||
let open: () -> Void
|
||||
let install: () -> Void
|
||||
let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
let itemContext: StickerPaneSearchGlobalItemContext
|
||||
public final class StickerPaneSearchGlobalItem: GridItem {
|
||||
public let account: Account
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let listAppearance: Bool
|
||||
public let fillsRow: Bool
|
||||
public let info: StickerPackCollectionInfo
|
||||
public let topItems: [StickerPackItem]
|
||||
public let topSeparator: Bool
|
||||
public let regularInsets: Bool
|
||||
public let installed: Bool
|
||||
public let installing: Bool
|
||||
public let unread: Bool
|
||||
public let open: () -> Void
|
||||
public let install: () -> Void
|
||||
public let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
public let itemContext: StickerPaneSearchGlobalItemContext
|
||||
|
||||
let section: GridSection?
|
||||
var fillsRowWithHeight: (CGFloat, Bool)? {
|
||||
public let section: GridSection?
|
||||
public var fillsRowWithHeight: (CGFloat, Bool)? {
|
||||
var additionalHeight: CGFloat = 0.0
|
||||
if self.regularInsets {
|
||||
additionalHeight = 12.0 + 12.0
|
||||
|
|
@ -106,7 +110,7 @@ final class StickerPaneSearchGlobalItem: GridItem {
|
|||
return (128.0 + additionalHeight, self.fillsRow)
|
||||
}
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
|
||||
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, listAppearance: Bool, fillsRow: Bool = true, info: StickerPackCollectionInfo, topItems: [StickerPackItem], topSeparator: Bool, regularInsets: Bool, installed: Bool, installing: Bool = false, unread: Bool, open: @escaping () -> Void, install: @escaping () -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool, itemContext: StickerPaneSearchGlobalItemContext, sectionTitle: String? = nil) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
|
@ -126,13 +130,13 @@ final class StickerPaneSearchGlobalItem: GridItem {
|
|||
self.section = StickerPaneSearchGlobalSection(title: sectionTitle, theme: theme)
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPaneSearchGlobalItemNode()
|
||||
node.setup(item: self)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
public func update(node: GridItemNode) {
|
||||
guard let node = node as? StickerPaneSearchGlobalItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
|
|
@ -145,7 +149,7 @@ private let titleFont = Font.bold(16.0)
|
|||
private let statusFont = Font.regular(15.0)
|
||||
private let buttonFont = Font.semibold(13.0)
|
||||
|
||||
class StickerPaneSearchGlobalItemNode: GridItemNode {
|
||||
public class StickerPaneSearchGlobalItemNode: GridItemNode {
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let unreadNode: ASImageNode
|
||||
|
|
@ -159,7 +163,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
private let topSeparatorNode: ASDisplayNode
|
||||
private var highlightNode: ASDisplayNode?
|
||||
|
||||
var item: StickerPaneSearchGlobalItem?
|
||||
public var item: StickerPaneSearchGlobalItem?
|
||||
private var appliedItem: StickerPaneSearchGlobalItem?
|
||||
private let preloadDisposable = MetaDisposable()
|
||||
private let preloadedStickerPackThumbnailDisposable = MetaDisposable()
|
||||
|
|
@ -175,7 +179,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override var isVisibleInGrid: Bool {
|
||||
public override var isVisibleInGrid: Bool {
|
||||
didSet {
|
||||
if oldValue != self.isVisibleInGrid {
|
||||
self.updatePlayback()
|
||||
|
|
@ -200,7 +204,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
public override init() {
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
|
|
@ -298,14 +302,14 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
self.preloadedStickerPackThumbnailDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
public override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
|
||||
self.absoluteLocation = (rect, containerSize)
|
||||
|
||||
for node in self.itemNodes {
|
||||
|
|
@ -314,7 +318,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
func setup(item: StickerPaneSearchGlobalItem) {
|
||||
public func setup(item: StickerPaneSearchGlobalItem) {
|
||||
if item.topItems.count < Int(item.info.count) && item.topItems.count < 5 && self.item?.info.id != item.info.id {
|
||||
self.preloadDisposable.set(preloadedFeaturedStickerSet(network: item.account.network, postbox: item.account.postbox, id: item.info.id).start())
|
||||
}
|
||||
|
|
@ -325,7 +329,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
self.updatePreviewing(animated: false)
|
||||
}
|
||||
|
||||
func updateCanPlayMedia() {
|
||||
public func updateCanPlayMedia() {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
|
@ -333,7 +337,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
self.canPlayMedia = item.itemContext.canPlayMedia
|
||||
}
|
||||
|
||||
func highlight() {
|
||||
public func highlight() {
|
||||
guard self.highlightNode == nil else {
|
||||
return
|
||||
}
|
||||
|
|
@ -354,7 +358,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
|
||||
public override func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
|
@ -521,7 +525,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
|
||||
public func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -535,7 +539,7 @@ class StickerPaneSearchGlobalItemNode: GridItemNode {
|
|||
return nil
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
public func updatePreviewing(animated: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import StickerResources
|
|||
import AccountContext
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class StickerPaneSearchStickerSection: GridSection {
|
||||
let code: String
|
||||
|
|
@ -65,16 +66,16 @@ final class StickerPaneSearchStickerSectionNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
final class StickerPaneSearchStickerItem: GridItem {
|
||||
let account: Account
|
||||
let code: String?
|
||||
let stickerItem: FoundStickerItem
|
||||
let selected: (ASDisplayNode, CGRect) -> Void
|
||||
let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
public final class StickerPaneSearchStickerItem: GridItem {
|
||||
public let account: Account
|
||||
public let code: String?
|
||||
public let stickerItem: FoundStickerItem
|
||||
public let selected: (ASDisplayNode, CGRect) -> Void
|
||||
public let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
let section: GridSection?
|
||||
public let section: GridSection?
|
||||
|
||||
init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) {
|
||||
public init(account: Account, code: String?, stickerItem: FoundStickerItem, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping (ASDisplayNode, CGRect) -> Void) {
|
||||
self.account = account
|
||||
self.stickerItem = stickerItem
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
|
@ -83,7 +84,7 @@ final class StickerPaneSearchStickerItem: GridItem {
|
|||
self.section = nil
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
public func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = StickerPaneSearchStickerItemNode()
|
||||
node.inputNodeInteraction = self.inputNodeInteraction
|
||||
node.setup(account: self.account, stickerItem: self.stickerItem, code: self.code)
|
||||
|
|
@ -91,7 +92,7 @@ final class StickerPaneSearchStickerItem: GridItem {
|
|||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
public func update(node: GridItemNode) {
|
||||
guard let node = node as? StickerPaneSearchStickerItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
|
|
@ -104,17 +105,17 @@ final class StickerPaneSearchStickerItem: GridItem {
|
|||
|
||||
private let textFont = Font.regular(20.0)
|
||||
|
||||
final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
public final class StickerPaneSearchStickerItemNode: GridItemNode {
|
||||
private var currentState: (Account, FoundStickerItem, CGSize)?
|
||||
let imageNode: TransformImageNode
|
||||
private(set) var animationNode: AnimatedStickerNode?
|
||||
public let imageNode: TransformImageNode
|
||||
public private(set) var animationNode: AnimatedStickerNode?
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
var currentIsPreviewing = false
|
||||
public var currentIsPreviewing = false
|
||||
|
||||
override var isVisibleInGrid: Bool {
|
||||
public override var isVisibleInGrid: Bool {
|
||||
didSet {
|
||||
self.updateVisibility()
|
||||
}
|
||||
|
|
@ -122,14 +123,14 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||
|
||||
private var isPlaying = false
|
||||
|
||||
var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
var selected: ((ASDisplayNode, CGRect) -> Void)?
|
||||
public var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
||||
public var selected: ((ASDisplayNode, CGRect) -> Void)?
|
||||
|
||||
var stickerItem: FoundStickerItem? {
|
||||
public var stickerItem: FoundStickerItem? {
|
||||
return self.currentState?.1
|
||||
}
|
||||
|
||||
override init() {
|
||||
public override init() {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
|
@ -145,7 +146,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
public override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.imageNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
|
|
@ -184,7 +185,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let bounds = self.bounds
|
||||
|
|
@ -211,11 +212,11 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||
self.selected?(self, self.bounds)
|
||||
}
|
||||
|
||||
func transitionNode() -> ASDisplayNode? {
|
||||
public func transitionNode() -> ASDisplayNode? {
|
||||
return self.imageNode
|
||||
}
|
||||
|
||||
func updateVisibility() {
|
||||
public func updateVisibility() {
|
||||
let isPlaying = self.isVisibleInGrid
|
||||
if self.isPlaying != isPlaying {
|
||||
self.isPlaying = isPlaying
|
||||
|
|
@ -223,7 +224,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
public func updatePreviewing(animated: Bool) {
|
||||
var isPreviewing = false
|
||||
if let (_, item, _) = self.currentState, let interaction = self.inputNodeInteraction {
|
||||
isPreviewing = interaction.previewedStickerPackItem == .found(item)
|
||||
|
|
@ -116,6 +116,10 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
switch action.action {
|
||||
case let .photoUpdated(image), let .suggestedProfilePhoto(image):
|
||||
if let peer = messageMainPeer(EngineMessage(message)), let image = image {
|
||||
var isSuggested = false
|
||||
if case .suggestedProfilePhoto = action.action {
|
||||
isSuggested = true
|
||||
}
|
||||
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false)])
|
||||
|
||||
let sourceCorners: AvatarGalleryController.SourceCorners
|
||||
|
|
@ -124,7 +128,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||
} else {
|
||||
sourceCorners = .round
|
||||
}
|
||||
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in
|
||||
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
|
||||
|
||||
})
|
||||
return .chatAvatars(galleryController, image)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public func composedDisplayTitle(strings: PresentationStrings) -> String {
|
||||
public func composedDisplayTitle(context: AccountContext, strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case let .personName(firstName, lastName, phone):
|
||||
if !firstName.isEmpty {
|
||||
|
|
@ -72,7 +72,7 @@ public enum ItemListAvatarAndNameInfoItemName: Equatable {
|
|||
} else if !lastName.isEmpty {
|
||||
return lastName
|
||||
} else if !phone.isEmpty {
|
||||
return formatPhoneNumber("+\(phone)")
|
||||
return formatPhoneNumber(context: context, number: "+\(phone)")
|
||||
} else {
|
||||
return strings.User_DeletedAccount
|
||||
}
|
||||
|
|
@ -395,7 +395,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
|
|||
nameMaximumNumberOfLines = 2
|
||||
}
|
||||
|
||||
let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (nameNodeLayout, nameNodeApply) = layoutNameNode(TextNodeLayoutArguments(attributedString: NSAttributedString(string: displayTitle.composedDisplayTitle(context: item.accountContext, strings: item.presentationData.strings), font: nameFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: nameMaximumNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: baseWidth - 20 - 94.0 - (item.call != nil ? 36.0 : 0.0) - additionalTitleInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var statusText: String = ""
|
||||
let statusColor: UIColor
|
||||
|
|
@ -404,7 +404,7 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
|
|||
switch item.mode {
|
||||
case .settings:
|
||||
if let phone = peer.phone, !phone.isEmpty {
|
||||
statusText += formatPhoneNumber(phone)
|
||||
statusText += formatPhoneNumber(context: item.accountContext, number: phone)
|
||||
}
|
||||
if let username = peer.addressName, !username.isEmpty {
|
||||
if !statusText.isEmpty {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public enum ItemListPeerActionItemColor {
|
|||
public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let icon: UIImage?
|
||||
let iconSignal: Signal<UIImage?, NoError>?
|
||||
let title: String
|
||||
public let alwaysPlain: Bool
|
||||
let hasSeparator: Bool
|
||||
|
|
@ -31,9 +32,10 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
|||
public let sectionId: ItemListSectionId
|
||||
let action: (() -> Void)?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
|
||||
self.presentationData = presentationData
|
||||
self.icon = icon
|
||||
self.iconSignal = iconSignal
|
||||
self.title = title
|
||||
self.alwaysPlain = alwaysPlain
|
||||
self.hasSeparator = hasSeparator
|
||||
|
|
@ -114,6 +116,8 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
|||
|
||||
private var item: ItemListPeerActionItem?
|
||||
|
||||
private let iconDisposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
|
@ -149,6 +153,10 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
|||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable.dispose()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
|
|
@ -171,7 +179,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
|||
iconOffset = 1.0
|
||||
verticalInset = 11.0
|
||||
verticalOffset = 0.0
|
||||
leftInset = (item.icon == nil ? 16.0 : 59.0) + params.leftInset
|
||||
leftInset = (item.icon == nil && item.iconSignal == nil ? 16.0 : 59.0) + params.leftInset
|
||||
case .peerList:
|
||||
iconOffset = 3.0
|
||||
verticalInset = 14.0
|
||||
|
|
@ -232,6 +240,15 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
|||
strongSelf.iconNode.image = item.icon
|
||||
if let image = item.icon {
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - image.size.width) / 2.0) + iconOffset, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size))
|
||||
} else if let iconSignal = item.iconSignal {
|
||||
let imageSize = CGSize(width: 28.0, height: 28.0)
|
||||
strongSelf.iconDisposable.set((iconSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
if let strongSelf = self, let image {
|
||||
strongSelf.iconNode.image = image
|
||||
}
|
||||
}))
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + editingOffset + floor((leftInset - params.leftInset - imageSize.width) / 2.0) + iconOffset, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize))
|
||||
}
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
- (UIImage *)paintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
- (UIImage *)stillPaintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)image forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;
|
||||
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;
|
||||
- (void)clearPaintingData;
|
||||
|
||||
- (SSignal *)facesForItem:(NSObject<TGMediaEditableItem> *)item;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
#import <LegacyComponents/TGModernGalleryImageItemImageView.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@class TGPhotoEntitiesContainerView;
|
||||
@protocol TGMediaEditableItem;
|
||||
@protocol TGPhotoDrawingEntitiesView;
|
||||
|
||||
@interface TGMediaPickerGalleryVideoItemView : TGModernGalleryItemView <TGModernGalleryEditableItemView>
|
||||
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
- (UIImage *)screenImage;
|
||||
- (UIImage *)transitionImage;
|
||||
- (CGRect)editorTransitionViewRect;
|
||||
- (TGPhotoEntitiesContainerView *)entitiesView;
|
||||
- (UIView<TGPhotoDrawingEntitiesView> *)entitiesView;
|
||||
|
||||
- (id<TGMediaEditableItem>)editableMediaItem;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,21 +8,20 @@
|
|||
@interface TGPaintingData : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSString *imagePath;
|
||||
@property (nonatomic, readonly) NSString *dataPath;
|
||||
@property (nonatomic, readonly) NSArray *entities;
|
||||
@property (nonatomic, readonly) TGPaintUndoManager *undoManager;
|
||||
@property (nonatomic, readonly) NSArray *stickers;
|
||||
|
||||
@property (nonatomic, readonly) NSData *data;
|
||||
@property (nonatomic, readonly) NSData *drawingData;
|
||||
@property (nonatomic, readonly) NSData *entitiesData;
|
||||
|
||||
@property (nonatomic, readonly) UIImage *image;
|
||||
|
||||
@property (nonatomic, readonly) UIImage *stillImage;
|
||||
|
||||
@property (nonatomic, readonly) NSArray *stickers;
|
||||
|
||||
@property (nonatomic, readonly) bool hasAnimation;
|
||||
|
||||
+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager;
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation;
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities;
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData;
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class PGPhotoEditorView;
|
||||
@class TGPhotoEntitiesContainerView;
|
||||
@protocol TGPhotoDrawingEntitiesView;
|
||||
|
||||
@interface TGPhotoAvatarCropView : UIView
|
||||
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
@property (nonatomic, readonly) bool isTracking;
|
||||
@property (nonatomic, readonly) bool isAnimating;
|
||||
|
||||
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square;
|
||||
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView<TGPhotoDrawingEntitiesView> *)fullEntitiesView square:(bool)square;
|
||||
|
||||
- (void)setSnapshotImage:(UIImage *)image;
|
||||
- (void)setSnapshotView:(UIView *)snapshotView;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
@class AVPlayer;
|
||||
|
||||
@protocol TGPhotoPaintStickersContext;
|
||||
@class TGPhotoEntitiesContainerView;
|
||||
@protocol TGPhotoDrawingEntitiesView;
|
||||
|
||||
typedef enum {
|
||||
TGPhotoEditorControllerGenericIntent = 0,
|
||||
|
|
@ -60,9 +60,11 @@ typedef enum {
|
|||
@property (nonatomic, strong) PGCameraShotMetadata *metadata;
|
||||
@property (nonatomic, strong) NSArray *faces;
|
||||
|
||||
@property (nonatomic, strong) NSString *senderName;
|
||||
|
||||
@property (nonatomic, strong) AVPlayer *player;
|
||||
|
||||
@property (nonatomic, strong) TGPhotoEntitiesContainerView *entitiesView;
|
||||
@property (nonatomic, strong) UIView<TGPhotoDrawingEntitiesView> *entitiesView;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab;
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@
|
|||
- (void)handlePinch:(UIPinchGestureRecognizer *)gestureRecognizer;
|
||||
- (void)handleRotate:(UIRotationGestureRecognizer *)gestureRecognizer;
|
||||
|
||||
- (void)setupWithEntitiesData:(NSData *)entitiesData;
|
||||
|
||||
@end
|
||||
|
||||
@protocol TGPhotoDrawingInterfaceController <NSObject>
|
||||
|
|
@ -134,6 +136,8 @@
|
|||
|
||||
|
||||
- (UIView<TGPhotoSolidRoundedButtonView> *)solidRoundedButton:(NSString *)title action:(void(^)(void))action;
|
||||
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size;
|
||||
- (id<TGPhotoDrawingAdapter>)drawingAdapter:(CGSize)size originalSize:(CGSize)originalSize;
|
||||
|
||||
- (UIView<TGPhotoDrawingEntitiesView> *)drawingEntitiesViewWithSize:(CGSize)size;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@interface TGPhotoVideoEditor : NSObject
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;
|
||||
|
||||
|
|
|
|||
|
|
@ -2969,14 +2969,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
if (adjustments.paintingData.stickers.count > 0)
|
||||
dict[@"stickers"] = adjustments.paintingData.stickers;
|
||||
|
||||
bool animated = false;
|
||||
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
|
||||
if (entity.animated) {
|
||||
animated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool animated = adjustments.paintingData.hasAnimation;
|
||||
if (animated) {
|
||||
dict[@"isAnimation"] = @true;
|
||||
if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) {
|
||||
|
|
|
|||
|
|
@ -929,10 +929,8 @@
|
|||
grouping = false;
|
||||
}
|
||||
}
|
||||
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
|
||||
if (entity.animated) {
|
||||
grouping = true;
|
||||
}
|
||||
if (adjustments.paintingData.hasAnimation) {
|
||||
grouping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1159,14 +1157,7 @@
|
|||
if (adjustments.paintingData.stickers.count > 0)
|
||||
dict[@"stickers"] = adjustments.paintingData.stickers;
|
||||
|
||||
bool animated = false;
|
||||
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
|
||||
if (entity.animated) {
|
||||
animated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool animated = adjustments.paintingData.hasAnimation;
|
||||
if (animated) {
|
||||
dict[@"isAnimation"] = @true;
|
||||
if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) {
|
||||
|
|
@ -1448,10 +1439,8 @@
|
|||
grouping = false;
|
||||
}
|
||||
}
|
||||
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
|
||||
if (entity.animated) {
|
||||
grouping = true;
|
||||
}
|
||||
if (adjustments.paintingData.hasAnimation) {
|
||||
grouping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@
|
|||
[_queue dispatch:block];
|
||||
}
|
||||
|
||||
- (bool)setPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video
|
||||
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video
|
||||
{
|
||||
NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier];
|
||||
|
||||
|
|
@ -725,6 +725,7 @@
|
|||
NSURL *imagesDirectory = video ? _videoPaintingImagesUrl : _paintingImagesUrl;
|
||||
NSURL *imageUrl = [imagesDirectory URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", [TGStringUtils md5:itemId]]];
|
||||
NSURL *dataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.dat", [TGStringUtils md5:itemId]]];
|
||||
NSURL *entitiesDataUrl = [_paintingDatasUrl URLByAppendingPathComponent:[NSString stringWithFormat:@"%@_entities.dat", [TGStringUtils md5:itemId]]];
|
||||
|
||||
[_paintingImageCache setImage:image forKey:itemId attributes:NULL];
|
||||
|
||||
|
|
@ -733,12 +734,16 @@
|
|||
bool imageSuccess = [imageData writeToURL:imageUrl options:NSDataWritingAtomic error:nil];
|
||||
[[NSFileManager defaultManager] removeItemAtURL:dataUrl error:nil];
|
||||
bool dataSuccess = [data writeToURL:dataUrl options:NSDataWritingAtomic error:nil];
|
||||
bool entitiesDataSuccess = [entitiesData writeToURL:entitiesDataUrl options:NSDataWritingAtomic error:nil];
|
||||
|
||||
if (imageSuccess && imageOutUrl != NULL)
|
||||
*imageOutUrl = imageUrl;
|
||||
|
||||
if (dataSuccess && dataOutUrl != NULL)
|
||||
*dataOutUrl = dataUrl;
|
||||
|
||||
if (entitiesDataSuccess && entitiesDataOutUrl != NULL)
|
||||
*entitiesDataOutUrl = entitiesDataUrl;
|
||||
|
||||
if (video)
|
||||
[_storeVideoPaintingImages addObject:imageUrl];
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@
|
|||
|
||||
#import <LegacyComponents/TGSecretTimerMenu.h>
|
||||
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
@interface TGMediaPickerGalleryModel ()
|
||||
{
|
||||
TGMediaPickerGalleryInterfaceView *_interfaceView;
|
||||
|
|
@ -363,7 +361,7 @@
|
|||
UIView *referenceParentView = nil;
|
||||
UIImage *image = nil;
|
||||
|
||||
TGPhotoEntitiesContainerView *entitiesView = nil;
|
||||
UIView<TGPhotoDrawingEntitiesView> *entitiesView = nil;
|
||||
|
||||
id<TGMediaEditableItem> editableMediaItem = item.editableMediaItem;
|
||||
|
||||
|
|
@ -373,7 +371,7 @@
|
|||
screenImage = [(UIImageView *)editorReferenceView image];
|
||||
referenceView = editorReferenceView;
|
||||
|
||||
if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject isKindOfClass:[TGPhotoEntitiesContainerView class]]) {
|
||||
if ([editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject conformsToProtocol:@protocol(TGPhotoDrawingEntitiesView)]) {
|
||||
entitiesView = editorReferenceView.subviews.firstObject.subviews.firstObject.subviews.firstObject;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@
|
|||
|
||||
#import "TGMediaPickerGalleryPhotoItem.h"
|
||||
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
|
||||
#import <LegacyComponents/TGMenuView.h>
|
||||
|
||||
|
|
@ -42,7 +41,7 @@
|
|||
|
||||
UIView *_contentView;
|
||||
UIView *_contentWrapperView;
|
||||
TGPhotoEntitiesContainerView *_entitiesContainerView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
|
||||
|
||||
SMetaDisposable *_adjustmentsDisposable;
|
||||
SMetaDisposable *_attributesDisposable;
|
||||
|
|
@ -90,11 +89,7 @@
|
|||
|
||||
_contentWrapperView = [[UIView alloc] init];
|
||||
[_contentView addSubview:_contentWrapperView];
|
||||
|
||||
_entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init];
|
||||
_entitiesContainerView.userInteractionEnabled = false;
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
|
||||
|
||||
_fileInfoLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 20)];
|
||||
_fileInfoLabel.backgroundColor = [UIColor clearColor];
|
||||
_fileInfoLabel.font = TGSystemFontOfSize(13);
|
||||
|
|
@ -143,7 +138,11 @@
|
|||
|
||||
[super setItem:item synchronously:synchronously];
|
||||
|
||||
_entitiesContainerView.stickersContext = item.stickersContext;
|
||||
if (_entitiesView == nil) {
|
||||
_entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.asset.originalSize];
|
||||
_entitiesView.userInteractionEnabled = false;
|
||||
[_contentWrapperView addSubview:_entitiesView];
|
||||
}
|
||||
|
||||
_imageSize = item.asset.originalSize;
|
||||
[self reset];
|
||||
|
|
@ -223,7 +222,7 @@
|
|||
return;
|
||||
|
||||
[strongSelf layoutEntities];
|
||||
[strongSelf->_entitiesContainerView setupWithPaintingData:next.paintingData];
|
||||
[strongSelf->_entitiesView setupWithEntitiesData:next.paintingData.entitiesData];
|
||||
}]];
|
||||
}
|
||||
|
||||
|
|
@ -486,27 +485,27 @@
|
|||
_contentView.transform = rotationTransform;
|
||||
_contentView.frame = previewFrame;
|
||||
|
||||
CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
|
||||
CGRect fittedCropRect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false];
|
||||
CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
|
||||
CGRect fittedCropRect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:false];
|
||||
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height);
|
||||
|
||||
CGFloat contentScale = _contentView.bounds.size.width / fittedCropRect.size.width;
|
||||
_contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale);
|
||||
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height);
|
||||
|
||||
CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
|
||||
_entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesContainerView.transform = CGAffineTransformMakeRotation(rotation);
|
||||
CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
|
||||
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesView.transform = CGAffineTransformMakeRotation(rotation);
|
||||
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation);
|
||||
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoPaintController fittedCropRect:cropRect centerScale:scale]);
|
||||
CGPoint offset = TGPaintSubtractPoints(centerPoint, [TGPhotoDrawingController fittedCropRect:cropRect centerScale:scale]);
|
||||
|
||||
CGPoint boundsCenter = TGPaintCenterOfRect(_contentWrapperView.bounds);
|
||||
_entitiesContainerView.center = TGPaintAddPoints(boundsCenter, offset);
|
||||
_entitiesView.center = TGPaintAddPoints(boundsCenter, offset);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@
|
|||
#import "TGMediaPickerScrubberHeaderView.h"
|
||||
|
||||
#import "TGPhotoEditorPreviewView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
|
||||
#import <LegacyComponents/TGModernGalleryVideoView.h>
|
||||
#import "TGModernGalleryVideoContentView.h"
|
||||
|
|
@ -84,7 +83,7 @@
|
|||
UIImageView *_paintingImageView;
|
||||
UIView *_contentView;
|
||||
UIView *_contentWrapperView;
|
||||
TGPhotoEntitiesContainerView *_entitiesContainerView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
|
||||
|
||||
NSTimer *_positionTimer;
|
||||
TGObserverProxy *_didPlayToEndObserver;
|
||||
|
|
@ -174,12 +173,7 @@
|
|||
|
||||
_contentWrapperView = [[UIView alloc] init];
|
||||
[_contentView addSubview:_contentWrapperView];
|
||||
|
||||
_entitiesContainerView = [[TGPhotoEntitiesContainerView alloc] init];
|
||||
_entitiesContainerView.hidden = true;
|
||||
_entitiesContainerView.userInteractionEnabled = false;
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
|
||||
|
||||
_curtainView = [[UIView alloc] init];
|
||||
_curtainView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_curtainView.backgroundColor = [UIColor blackColor];
|
||||
|
|
@ -425,8 +419,14 @@
|
|||
|
||||
_scrubberView.allowsTrimming = false;
|
||||
_videoDimensions = item.dimensions;
|
||||
_entitiesContainerView.stickersContext = item.stickersContext;
|
||||
|
||||
if (_entitiesView == nil) {
|
||||
_entitiesView = [item.stickersContext drawingEntitiesViewWithSize:item.dimensions];
|
||||
_entitiesView.hidden = true;
|
||||
_entitiesView.userInteractionEnabled = false;
|
||||
[_contentWrapperView addSubview:_entitiesView];
|
||||
}
|
||||
|
||||
__weak TGMediaPickerGalleryVideoItemView *weakSelf = self;
|
||||
[_videoDurationVar set:[[[item.durationSignal deliverOn:[SQueue mainQueue]] catch:^SSignal *(__unused id error)
|
||||
{
|
||||
|
|
@ -504,8 +504,8 @@
|
|||
if (baseAdjustments.sendAsGif || ([strongSelf itemIsLivePhoto]))
|
||||
[strongSelf setPlayButtonHidden:true animated:false];
|
||||
|
||||
[strongSelf->_entitiesContainerView setupWithPaintingData:adjustments.paintingData];
|
||||
[strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying];
|
||||
[strongSelf->_entitiesView setupWithEntitiesData:adjustments.paintingData.entitiesData];
|
||||
[strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying];
|
||||
[strongSelf->_photoEditor importAdjustments:adjustments];
|
||||
|
||||
if (!strongSelf.isPlaying) {
|
||||
|
|
@ -823,24 +823,24 @@
|
|||
_contentView.transform = rotationTransform;
|
||||
_contentView.frame = _imageView.frame;
|
||||
|
||||
CGSize fittedContentSize = [TGPhotoPaintController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
|
||||
CGSize fittedContentSize = [TGPhotoDrawingController fittedContentSize:cropRect orientation:orientation originalSize:originalSize];
|
||||
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, fittedContentSize.width, fittedContentSize.height);
|
||||
|
||||
CGFloat contentScale = ratio;
|
||||
_contentWrapperView.transform = CGAffineTransformMakeScale(contentScale, contentScale);
|
||||
_contentWrapperView.frame = CGRectMake(0.0f, 0.0f, _contentView.bounds.size.width, _contentView.bounds.size.height);
|
||||
|
||||
CGRect rect = [TGPhotoPaintController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
|
||||
_entitiesContainerView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesContainerView.transform = CGAffineTransformMakeRotation(0.0);
|
||||
CGRect rect = [TGPhotoDrawingController fittedCropRect:cropRect originalSize:originalSize keepOriginalSize:true];
|
||||
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesView.transform = CGAffineTransformMakeRotation(0.0);
|
||||
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, 0.0);
|
||||
__unused CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
}
|
||||
|
||||
- (TGPhotoEntitiesContainerView *)entitiesView {
|
||||
return _entitiesContainerView;
|
||||
- (UIView<TGPhotoDrawingEntitiesView> *)entitiesView {
|
||||
return _entitiesView;
|
||||
}
|
||||
|
||||
- (void)singleTap
|
||||
|
|
@ -1176,7 +1176,7 @@
|
|||
strongSelf->_videoView.userInteractionEnabled = false;
|
||||
[strongSelf->_playerContainerView insertSubview:strongSelf->_videoView belowSubview:strongSelf->_paintingImageView];
|
||||
|
||||
strongSelf->_entitiesContainerView.hidden = false;
|
||||
strongSelf->_entitiesView.hidden = false;
|
||||
|
||||
[strongSelf->_videoView setNeedsTransitionIn];
|
||||
[strongSelf->_videoView performTransitionInIfNeeded];
|
||||
|
|
@ -1196,7 +1196,7 @@
|
|||
[strongSelf->_player play];
|
||||
}
|
||||
|
||||
[strongSelf->_entitiesContainerView updateVisibility:strongSelf.isPlaying];
|
||||
[strongSelf->_entitiesView updateVisibility:strongSelf.isPlaying];
|
||||
|
||||
strongSelf->_positionTimer = [TGTimerTarget scheduledMainThreadTimerWithTarget:self action:@selector(positionTimerEvent) interval:0.25 repeat:true];
|
||||
[strongSelf positionTimerEvent];
|
||||
|
|
@ -1211,11 +1211,11 @@
|
|||
{
|
||||
if (_player.rate > FLT_EPSILON) {
|
||||
[_scrubberView setIsPlaying:true];
|
||||
[_entitiesContainerView updateVisibility:true];
|
||||
[_entitiesView updateVisibility:true];
|
||||
}
|
||||
else {
|
||||
[_scrubberView setIsPlaying:false];
|
||||
[_entitiesContainerView updateVisibility:false];
|
||||
[_entitiesView updateVisibility:false];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1251,7 +1251,7 @@
|
|||
[self positionTimerEvent];
|
||||
}
|
||||
|
||||
[_entitiesContainerView updateVisibility:true];
|
||||
[_entitiesView updateVisibility:true];
|
||||
}
|
||||
|
||||
- (void)playIfAvailable
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
[_positionTimer invalidate];
|
||||
_positionTimer = nil;
|
||||
|
||||
[_entitiesContainerView updateVisibility:false];
|
||||
[_entitiesView updateVisibility:false];
|
||||
}
|
||||
|
||||
- (void)togglePlayback
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
{
|
||||
UIImage *_image;
|
||||
UIImage *_stillImage;
|
||||
NSData *_data;
|
||||
|
||||
UIImage *(^_imageRetrievalBlock)(void);
|
||||
UIImage *(^_stillImageRetrievalBlock)(void);
|
||||
|
|
@ -21,21 +20,21 @@
|
|||
|
||||
@implementation TGPaintingData
|
||||
|
||||
+ (instancetype)dataWithPaintingData:(NSData *)data image:(UIImage *)image stillImage:(UIImage *)stillImage entities:(NSArray *)entities undoManager:(TGPaintUndoManager *)undoManager
|
||||
+ (instancetype)dataWithDrawingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage hasAnimation:(bool)hasAnimation
|
||||
{
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_data = data;
|
||||
paintingData->_drawingData = data;
|
||||
paintingData->_image = image;
|
||||
paintingData->_stillImage = stillImage;
|
||||
paintingData->_entities = entities;
|
||||
paintingData->_undoManager = undoManager;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
paintingData->_hasAnimation = hasAnimation;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entities:(NSArray *)entities {
|
||||
+ (instancetype)dataWithPaintingImagePath:(NSString *)imagePath entitiesData:(NSData *)entitiesData {
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_imagePath = imagePath;
|
||||
paintingData->_entities = entities;
|
||||
paintingData->_entitiesData = entitiesData;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +48,7 @@
|
|||
- (instancetype)dataForAnimation
|
||||
{
|
||||
TGPaintingData *paintingData = [[TGPaintingData alloc] init];
|
||||
paintingData->_entities = _entities;
|
||||
paintingData->_entitiesData = _entitiesData;
|
||||
return paintingData;
|
||||
}
|
||||
|
||||
|
|
@ -58,17 +57,17 @@
|
|||
[[TGPaintingData queue] dispatch:^
|
||||
{
|
||||
NSURL *dataUrl = nil;
|
||||
NSURL *entitiesDataUrl = nil;
|
||||
NSURL *imageUrl = nil;
|
||||
|
||||
NSData *compressedData = TGPaintGZipDeflate(data.data);
|
||||
[context setPaintingData:compressedData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl imageUrl:&imageUrl forVideo:video];
|
||||
NSData *compressedDrawingData = TGPaintGZipDeflate(data.drawingData);
|
||||
NSData *compressedEntitiesData = TGPaintGZipDeflate(data.entitiesData);
|
||||
[context setPaintingData:compressedDrawingData entitiesData:compressedEntitiesData image:data.image stillImage:data.stillImage forItem:item dataUrl:&dataUrl entitiesDataUrl:&entitiesDataUrl imageUrl:&imageUrl forVideo:video];
|
||||
|
||||
__weak TGMediaEditingContext *weakContext = context;
|
||||
[[SQueue mainQueue] dispatch:^
|
||||
{
|
||||
data->_dataPath = dataUrl.path;
|
||||
data->_imagePath = imageUrl.path;
|
||||
data->_data = nil;
|
||||
|
||||
data->_imageRetrievalBlock = ^UIImage *
|
||||
{
|
||||
|
|
@ -100,20 +99,6 @@
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self.undoManager reset];
|
||||
}
|
||||
|
||||
- (NSData *)data
|
||||
{
|
||||
if (_data != nil)
|
||||
return _data;
|
||||
else if (_dataPath != nil)
|
||||
return TGPaintGZipInflate([[NSData alloc] initWithContentsOfFile:_dataPath]);
|
||||
else
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIImage *)image
|
||||
{
|
||||
|
|
@ -128,7 +113,7 @@
|
|||
- (UIImage *)stillImage
|
||||
{
|
||||
if (_stillImage != nil)
|
||||
return _stillImage;
|
||||
return _stillImage;
|
||||
else if (_stillImageRetrievalBlock != nil)
|
||||
return _stillImageRetrievalBlock();
|
||||
else
|
||||
|
|
@ -137,23 +122,14 @@
|
|||
|
||||
- (NSArray *)stickers
|
||||
{
|
||||
NSMutableSet *stickers = [[NSMutableSet alloc] init];
|
||||
for (TGPhotoPaintEntity *entity in self.entities)
|
||||
{
|
||||
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
|
||||
[stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
|
||||
}
|
||||
return [stickers allObjects];
|
||||
}
|
||||
|
||||
- (bool)hasAnimation
|
||||
{
|
||||
for (TGPhotoPaintEntity *entity in self.entities)
|
||||
{
|
||||
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]] && ((TGPhotoPaintStickerEntity *)entity).animated)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return @[];
|
||||
// NSMutableSet *stickers = [[NSMutableSet alloc] init];
|
||||
// for (TGPhotoPaintEntity *entity in self.entities)
|
||||
// {
|
||||
// if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]])
|
||||
// [stickers addObject:((TGPhotoPaintStickerEntity *)entity).document];
|
||||
// }
|
||||
// return [stickers allObjects];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
|
|
@ -165,7 +141,7 @@
|
|||
return false;
|
||||
|
||||
TGPaintingData *data = (TGPaintingData *)object;
|
||||
return [data.entities isEqual:self.entities] && ((data.data != nil && [data.data isEqualToData:self.data]) || (data.data == nil && self.data == nil));
|
||||
return [data.entitiesData isEqual:self.entitiesData] && ((data.drawingData != nil && [data.drawingData isEqualToData:self.drawingData]) || (data.drawingData == nil && self.drawingData == nil));
|
||||
}
|
||||
|
||||
+ (SQueue *)queue
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
#import "TGPhotoEditorInterfaceAssets.h"
|
||||
|
||||
#import "PGPhotoEditorView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
const CGFloat TGPhotoAvatarCropViewOverscreenSize = 1000;
|
||||
const CGFloat TGPhotoAvatarCropViewCurtainSize = 300;
|
||||
|
|
@ -46,13 +45,13 @@ const CGFloat TGPhotoAvatarCropViewCurtainMargin = 200;
|
|||
|
||||
__weak PGPhotoEditorView *_fullPreviewView;
|
||||
__weak UIImageView *_fullPaintingView;
|
||||
__weak TGPhotoEntitiesContainerView *_fullEntitiesView;
|
||||
__weak UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation TGPhotoAvatarCropView
|
||||
|
||||
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(TGPhotoEntitiesContainerView *)fullEntitiesView square:(bool)square
|
||||
- (instancetype)initWithOriginalSize:(CGSize)originalSize screenSize:(CGSize)screenSize fullPreviewView:(PGPhotoEditorView *)fullPreviewView fullPaintingView:(UIImageView *)fullPaintingView fullEntitiesView:(UIView<TGPhotoDrawingEntitiesView> *)fullEntitiesView square:(bool)square
|
||||
{
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self != nil)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
@class PGPhotoEditor;
|
||||
@class PGPhotoTool;
|
||||
@class TGPhotoEditorPreviewView;
|
||||
@class TGPhotoEntitiesContainerView;
|
||||
@class PGPhotoEditorView;
|
||||
@class TGMediaPickerGalleryVideoScrubber;
|
||||
|
||||
|
|
@ -23,12 +22,12 @@
|
|||
@property (nonatomic, weak) UIView *dotMarkerView;
|
||||
@property (nonatomic, weak) PGPhotoEditorView *fullPreviewView;
|
||||
@property (nonatomic, weak) UIImageView *fullPaintingView;
|
||||
@property (nonatomic, weak) TGPhotoEntitiesContainerView *fullEntitiesView;
|
||||
@property (nonatomic, weak) UIView<TGPhotoDrawingEntitiesView> *fullEntitiesView;
|
||||
@property (nonatomic, weak) TGMediaPickerGalleryVideoScrubber *scrubberView;
|
||||
|
||||
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion;
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion senderName:(NSString *)senderName;
|
||||
|
||||
- (void)setImage:(UIImage *)image;
|
||||
- (void)setSnapshotImage:(UIImage *)snapshotImage;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
#import "TGMediaPickerGalleryVideoScrubber.h"
|
||||
#import "TGModernGalleryVideoView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
|
||||
#import "TGPhotoPaintStickersContext.h"
|
||||
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
|
||||
const CGFloat TGPhotoAvatarPreviewPanelSize = 96.0f;
|
||||
const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanelSize + 40.0f;
|
||||
|
|
@ -48,6 +48,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
TGModernButton *_cancelButton;
|
||||
UILabel *_titleLabel;
|
||||
UILabel *_subtitleLabel;
|
||||
UIView<TGPhotoSolidRoundedButtonView> *_doneButton;
|
||||
|
||||
bool _wasPlayingBeforeCropping;
|
||||
|
|
@ -56,6 +57,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
bool _isForum;
|
||||
bool _isSuggestion;
|
||||
NSString *_senderName;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) PGPhotoEditor *photoEditor;
|
||||
|
|
@ -65,7 +67,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
@implementation TGPhotoAvatarPreviewController
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion {
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView isForum:(bool)isForum isSuggestion:(bool)isSuggestion senderName:(NSString *)senderName {
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
{
|
||||
|
|
@ -73,6 +75,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
self.previewView = previewView;
|
||||
_isForum = isForum;
|
||||
_isSuggestion = isSuggestion;
|
||||
_senderName = senderName;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
@ -181,7 +184,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_coverLabel.backgroundColor = [UIColor clearColor];
|
||||
_coverLabel.font = TGSystemFontOfSize(14.0f);
|
||||
_coverLabel.textColor = [UIColor whiteColor];
|
||||
_coverLabel.text = TGLocalized(@"PhotoEditor.SelectCoverFrame");
|
||||
_coverLabel.text = _isSuggestion ? TGLocalized(@"PhotoEditor.SelectCoverFrameSuggestion") : TGLocalized(@"PhotoEditor.SelectCoverFrame");
|
||||
[_coverLabel sizeToFit];
|
||||
[_portraitToolsWrapperView addSubview:_coverLabel];
|
||||
|
||||
|
|
@ -193,10 +196,19 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_titleLabel.backgroundColor = [UIColor clearColor];
|
||||
_titleLabel.font = TGBoldSystemFontOfSize(17.0f);
|
||||
_titleLabel.textColor = [UIColor whiteColor];
|
||||
_titleLabel.text = self.item.isVideo ? TGLocalized(@"PhotoEditor.SetProfileVideo") : TGLocalized(@"PhotoEditor.SetProfilePhoto");
|
||||
_titleLabel.text = self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoTitle") : TGLocalized(@"Conversation.SuggestedPhotoTitle");
|
||||
[_titleLabel sizeToFit];
|
||||
[_wrapperView addSubview:_titleLabel];
|
||||
|
||||
_subtitleLabel = [[UILabel alloc] init];
|
||||
_subtitleLabel.backgroundColor = [UIColor clearColor];
|
||||
_subtitleLabel.font = TGSystemFontOfSize(14.0f);
|
||||
_subtitleLabel.textColor = [UIColor whiteColor];
|
||||
_subtitleLabel.numberOfLines = 2;
|
||||
_subtitleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_subtitleLabel.text = [NSString stringWithFormat:self.item.isVideo ? TGLocalized(@"Conversation.SuggestedVideoText") : TGLocalized(@"Conversation.SuggestedPhotoText"), _senderName];
|
||||
[_wrapperView addSubview:_subtitleLabel];
|
||||
|
||||
if (!self.item.isVideo) {
|
||||
_cancelButton = [[TGModernButton alloc] init];
|
||||
[_cancelButton setTitle:TGLocalized(@"Common.Cancel") forState:UIControlStateNormal];
|
||||
|
|
@ -369,15 +381,17 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
[_cropView animateTransitionIn];
|
||||
|
||||
_cancelButton.alpha = 0.0;
|
||||
_titleLabel.alpha = 0.0;
|
||||
_doneButton.alpha = 0.0;
|
||||
_cancelButton.alpha = 0.0f;
|
||||
_titleLabel.alpha = 0.0f;
|
||||
_subtitleLabel.alpha = 0.0f;
|
||||
_doneButton.alpha = 0.0f;
|
||||
|
||||
[UIView animateWithDuration:0.3f animations:^
|
||||
{
|
||||
_cancelButton.alpha = 1.0;
|
||||
_titleLabel.alpha = 1.0;
|
||||
_doneButton.alpha = 1.0;
|
||||
_cancelButton.alpha = 1.0f;
|
||||
_titleLabel.alpha = 1.0f;
|
||||
_subtitleLabel.alpha = 1.0f;
|
||||
_doneButton.alpha = 1.0f;
|
||||
|
||||
_portraitToolsWrapperView.alpha = 1.0f;
|
||||
_landscapeToolsWrapperView.alpha = 1.0f;
|
||||
|
|
@ -468,7 +482,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
if (self.switchingToTab == TGPhotoEditorPaintTab)
|
||||
{
|
||||
containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:referenceBounds toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
}
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(cropRectFrame.size, containerFrame.size);
|
||||
|
|
@ -551,9 +565,10 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_landscapeToolsWrapperView.alpha = 0.0f;
|
||||
_dotImageView.alpha = 0.0f;
|
||||
_dotMarkerView.alpha = 0.0f;
|
||||
_cancelButton.alpha = 0.0;
|
||||
_titleLabel.alpha = 0.0;
|
||||
_doneButton.alpha = 0.0;
|
||||
_cancelButton.alpha = 0.0f;
|
||||
_titleLabel.alpha = 0.0f;
|
||||
_subtitleLabel.alpha = 0.0f;
|
||||
_doneButton.alpha = 0.0f;
|
||||
} completion:^(__unused BOOL finished)
|
||||
{
|
||||
if (!switching) {
|
||||
|
|
@ -670,8 +685,9 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_landscapeToolsWrapperView.alpha = 0.0f;
|
||||
_dotImageView.alpha = 0.0f;
|
||||
_titleLabel.alpha = 0.0f;
|
||||
_subtitleLabel.alpha = 0.0f;
|
||||
_cancelButton.alpha = 0.0f;
|
||||
_doneButton.alpha = 0.0;
|
||||
_doneButton.alpha = 0.0f;
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
|
|
@ -804,6 +820,8 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
CGSize buttonSize = CGSizeMake(MIN(referenceSize.width, referenceSize.height) - 16.0 * 2.0, 50.0f);
|
||||
[_doneButton updateWidth:buttonSize.width];
|
||||
|
||||
CGSize subtitleSize = [_subtitleLabel sizeThatFits:CGSizeMake(referenceSize.width - 48.0, referenceSize.height)];
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
|
|
@ -820,6 +838,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
|
||||
|
||||
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
|
||||
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height);
|
||||
|
||||
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
|
||||
|
||||
|
|
@ -841,6 +860,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_portraitToolsWrapperView.frame = CGRectMake((screenSide - referenceSize.width) / 2, screenSide - panelToolbarPortraitSize, referenceSize.width, panelToolbarPortraitSize);
|
||||
|
||||
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), 0.0, _titleLabel.frame.size.width, _titleLabel.frame.size.height);
|
||||
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom + safeAreaInset.bottom, subtitleSize.width, subtitleSize.height);
|
||||
|
||||
_cancelButton.frame = CGRectMake(-_cancelButton.frame.size.width, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
|
||||
|
||||
|
|
@ -867,6 +887,7 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
_coverLabel.frame = CGRectMake(floor((_portraitToolsWrapperView.frame.size.width - _coverLabel.frame.size.width) / 2.0), CGRectGetMaxY(_scrubberView.frame) + 6.0, _coverLabel.frame.size.width, _coverLabel.frame.size.height);
|
||||
|
||||
_titleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _titleLabel.frame.size.width) / 2.0), screenEdges.top + floor((44.0 - _titleLabel.frame.size.height) / 2.0), _titleLabel.frame.size.width, _titleLabel.frame.size.height);
|
||||
_subtitleLabel.frame = CGRectMake(screenEdges.left + floor((referenceSize.width - _subtitleLabel.frame.size.width) / 2.0), screenEdges.bottom - 56.0 - buttonSize.height - subtitleSize.height - 8.0, subtitleSize.width, subtitleSize.height);
|
||||
|
||||
_cancelButton.frame = CGRectMake(screenEdges.left + 16.0, screenEdges.top + floor((44.0 - _cancelButton.frame.size.height) / 2.0), _cancelButton.frame.size.width, _cancelButton.frame.size.height);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,18 @@
|
|||
@property (nonatomic, copy) void (^requestDismiss)(void);
|
||||
@property (nonatomic, copy) void (^requestApply)(void);
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext;
|
||||
|
||||
- (TGPaintingData *)paintingData;
|
||||
|
||||
+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
|
||||
|
||||
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
|
||||
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
|
||||
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
|
||||
+ (CGSize)maximumPaintingSize;
|
||||
|
||||
@end
|
||||
|
||||
extern const CGFloat TGPhotoPaintTopPanelSize;
|
||||
extern const CGFloat TGPhotoPaintBottomPanelSize;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#import "TGPhotoDrawingController.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
|
||||
#import "LegacyComponentsInternal.h"
|
||||
|
||||
|
|
@ -21,17 +20,18 @@
|
|||
|
||||
#import "TGPaintingWrapperView.h"
|
||||
#import "TGPhotoEditorSparseView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
#import "PGPhotoEditor.h"
|
||||
#import "TGPhotoEditorPreviewView.h"
|
||||
|
||||
|
||||
|
||||
|
||||
#import "TGPaintCanvas.h"
|
||||
#import "TGPainting.h"
|
||||
|
||||
const CGFloat TGPhotoPaintTopPanelSize = 44.0f;
|
||||
const CGFloat TGPhotoPaintBottomPanelSize = 79.0f;
|
||||
const CGSize TGPhotoPaintingLightMaxSize = { 1280.0f, 1280.0f };
|
||||
const CGSize TGPhotoPaintingMaxSize = { 2560.0f, 2560.0f };
|
||||
|
||||
@interface TGPhotoDrawingController () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
||||
{
|
||||
id<LegacyComponentsContext> _context;
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
@implementation TGPhotoDrawingController
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext
|
||||
{
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
|
|
@ -84,8 +84,8 @@
|
|||
_context = context;
|
||||
_stickersContext = stickersContext;
|
||||
|
||||
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
_drawingAdapter = [_stickersContext drawingAdapter:size];
|
||||
CGSize size = TGScaleToSize(photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
_drawingAdapter = [_stickersContext drawingAdapter:size originalSize:photoEditor.originalSize];
|
||||
_interfaceController = (UIViewController<TGPhotoDrawingInterfaceController> *)_drawingAdapter.interfaceController;
|
||||
|
||||
__weak TGPhotoDrawingController *weakSelf = self;
|
||||
|
|
@ -218,6 +218,16 @@
|
|||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
PGPhotoEditor *photoEditor = _photoEditor;
|
||||
if (!_skipEntitiesSetup) {
|
||||
[_entitiesView setupWithEntitiesData:photoEditor.paintingData.entitiesData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
|
@ -292,11 +302,11 @@
|
|||
|
||||
- (CGSize)fittedContentSize
|
||||
{
|
||||
return [TGPhotoPaintController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize];
|
||||
return [TGPhotoDrawingController fittedContentSize:_photoEditor.cropRect orientation:_photoEditor.cropOrientation originalSize:_photoEditor.originalSize];
|
||||
}
|
||||
|
||||
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize {
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
|
||||
CGSize size = CGSizeMake(cropRect.size.width * scale, cropRect.size.height * scale);
|
||||
|
|
@ -308,11 +318,11 @@
|
|||
|
||||
- (CGRect)fittedCropRect:(bool)originalSize
|
||||
{
|
||||
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize];
|
||||
return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:originalSize];
|
||||
}
|
||||
|
||||
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)keepOriginalSize {
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
|
||||
CGSize size = fittedOriginalSize;
|
||||
|
|
@ -324,7 +334,7 @@
|
|||
|
||||
- (CGPoint)fittedCropCenterScale:(CGFloat)scale
|
||||
{
|
||||
return [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect centerScale:scale];
|
||||
return [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect centerScale:scale];
|
||||
}
|
||||
|
||||
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale
|
||||
|
|
@ -488,7 +498,7 @@
|
|||
- (CGRect)_targetFrameForTransitionInFromFrame:(CGRect)fromFrame
|
||||
{
|
||||
CGSize referenceSize = [self referenceViewSize];
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(fromFrame.size, containerFrame.size);
|
||||
CGRect toFrame = CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
|
@ -518,7 +528,7 @@
|
|||
_entitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
_entitiesView.transform = CGAffineTransformMakeRotation(_photoEditor.cropRotation);
|
||||
|
||||
CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoPaintController maximumPaintingSize]);
|
||||
CGSize fittedOriginalSize = TGScaleToSize(_photoEditor.originalSize, [TGPhotoDrawingController maximumPaintingSize]);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, _photoEditor.cropRotation);
|
||||
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
|
||||
|
|
@ -554,7 +564,7 @@
|
|||
|
||||
- (CGRect)transitionOutSourceFrameForReferenceFrame:(CGRect)referenceFrame orientation:(UIInterfaceOrientation)orientation
|
||||
{
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(referenceFrame.size, containerFrame.size);
|
||||
return CGRectMake(containerFrame.origin.x + (containerFrame.size.width - fittedSize.width) / 2, containerFrame.origin.y + (containerFrame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
|
@ -571,7 +581,7 @@
|
|||
[previewView prepareForTransitionOut];
|
||||
|
||||
UIInterfaceOrientation orientation = self.effectiveOrientation;
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:self.view.frame toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect referenceFrame = CGRectMake(0, 0, self.photoEditor.rotatedCropSize.width, self.photoEditor.rotatedCropSize.height);
|
||||
CGRect rect = CGRectOffset([self transitionOutSourceFrameForReferenceFrame:referenceFrame orientation:orientation], -containerFrame.origin.x, -containerFrame.origin.y);
|
||||
previewView.frame = rect;
|
||||
|
|
@ -745,7 +755,7 @@
|
|||
CGSize referenceSize = [self referenceViewSize];
|
||||
CGFloat screenSide = MAX(referenceSize.width, referenceSize.height) + 2 * TGPhotoPaintBottomPanelSize;
|
||||
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:self.effectiveOrientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
CGFloat topInset = [self controllerStatusBarHeight] + 31.0;
|
||||
CGFloat visibleArea = self.view.frame.size.height - _keyboardHeight - topInset;
|
||||
|
|
@ -800,7 +810,7 @@
|
|||
screenEdges.bottom -= safeAreaInset.bottom;
|
||||
screenEdges.right -= safeAreaInset.right;
|
||||
|
||||
CGRect containerFrame = [TGPhotoPaintController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
CGRect containerFrame = [TGPhotoDrawingController photoContainerFrameForParentViewFrame:CGRectMake(0, 0, referenceSize.width, referenceSize.height) toolbarLandscapeSize:self.toolbarLandscapeSize orientation:orientation panelSize:TGPhotoPaintTopPanelSize + TGPhotoPaintBottomPanelSize hasOnScreenNavigation:self.hasOnScreenNavigation];
|
||||
|
||||
PGPhotoEditor *photoEditor = self.photoEditor;
|
||||
TGPhotoEditorPreviewView *previewView = self.previewView;
|
||||
|
|
@ -880,4 +890,19 @@
|
|||
return UIRectEdgeTop | UIRectEdgeBottom;
|
||||
}
|
||||
|
||||
+ (CGSize)maximumPaintingSize
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static CGSize size;
|
||||
dispatch_once(&onceToken, ^
|
||||
{
|
||||
CGSize screenSize = TGScreenSize();
|
||||
if ((NSInteger)screenSize.height == 480)
|
||||
size = TGPhotoPaintingLightMaxSize;
|
||||
else
|
||||
size = TGPhotoPaintingMaxSize;
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
|
||||
#import "TGPhotoToolbarView.h"
|
||||
#import "TGPhotoEditorPreviewView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
#import <LegacyComponents/TGMenuView.h>
|
||||
|
||||
|
|
@ -38,7 +37,6 @@
|
|||
|
||||
#import "TGPhotoCropController.h"
|
||||
#import "TGPhotoToolsController.h"
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
#import "TGPhotoQualityController.h"
|
||||
#import "TGPhotoAvatarPreviewController.h"
|
||||
|
|
@ -71,7 +69,7 @@
|
|||
TGPhotoToolbarView *_landscapeToolbarView;
|
||||
TGPhotoEditorPreviewView *_previewView;
|
||||
PGPhotoEditorView *_fullPreviewView;
|
||||
TGPhotoEntitiesContainerView *_fullEntitiesView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
|
||||
UIImageView *_fullPaintingView;
|
||||
|
||||
PGPhotoEditor *_photoEditor;
|
||||
|
|
@ -288,10 +286,7 @@
|
|||
|
||||
case TGPhotoEditorPaintTab:
|
||||
case TGPhotoEditorEraserTab:
|
||||
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
[strongSelf->_currentTabController handleTabAction:tab];
|
||||
else
|
||||
[strongSelf presentTab:TGPhotoEditorPaintTab];
|
||||
[strongSelf presentTab:TGPhotoEditorPaintTab];
|
||||
break;
|
||||
|
||||
case TGPhotoEditorStickerTab:
|
||||
|
|
@ -353,9 +348,9 @@
|
|||
_fullPaintingView = [[UIImageView alloc] init];
|
||||
_fullPaintingView.frame = _fullPreviewView.frame;
|
||||
|
||||
_fullEntitiesView = [[TGPhotoEntitiesContainerView alloc] init];
|
||||
_fullEntitiesView = [_stickersContext drawingEntitiesViewWithSize:CGSizeMake(0, 0)];
|
||||
_fullEntitiesView.userInteractionEnabled = false;
|
||||
CGRect rect = [TGPhotoPaintController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true];
|
||||
CGRect rect = [TGPhotoDrawingController fittedCropRect:_photoEditor.cropRect originalSize:_photoEditor.originalSize keepOriginalSize:true];
|
||||
_fullEntitiesView.frame = CGRectMake(0, 0, rect.size.width, rect.size.height);
|
||||
}
|
||||
|
||||
|
|
@ -1341,7 +1336,7 @@
|
|||
{
|
||||
bool skipInitialTransition = (![self presentedFromCamera] && self.navigationController != nil) || self.skipInitialTransition;
|
||||
|
||||
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar]];
|
||||
TGPhotoAvatarPreviewController *cropController = [[TGPhotoAvatarPreviewController alloc] initWithContext:_context photoEditor:_photoEditor previewView:_previewView isForum:[self presentedForForumAvatarCreation] isSuggestion:[self presentedForSuggestedAvatar] senderName:self.senderName];
|
||||
cropController.stickersContext = _stickersContext;
|
||||
cropController.scrubberView = _scrubberView;
|
||||
cropController.dotImageView = _dotImageView;
|
||||
|
|
|
|||
|
|
@ -173,10 +173,10 @@
|
|||
|
||||
- (void)setupWithPaintingData:(TGPaintingData *)paintingData {
|
||||
[self removeAll];
|
||||
for (TGPhotoPaintEntity *entity in paintingData.entities) {
|
||||
UIView * entityView = [self createEntityViewWithEntity:entity];
|
||||
[self addSubview:entityView];
|
||||
}
|
||||
// for (TGPhotoPaintEntity *entity in paintingData.entities) {
|
||||
// UIView * entityView = [self createEntityViewWithEntity:entity];
|
||||
// [self addSubview:entityView];
|
||||
// }
|
||||
}
|
||||
|
||||
- (TGPhotoPaintEntityView *)createEntityViewWithEntity:(TGPhotoPaintEntity *)entity {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
#import "TGPhotoEditorTabController.h"
|
||||
|
||||
#import <LegacyComponents/LegacyComponentsContext.h>
|
||||
|
||||
@class PGPhotoEditor;
|
||||
@class TGPhotoEditorPreviewView;
|
||||
|
||||
@protocol TGPhotoPaintStickersContext;
|
||||
|
||||
@interface TGPhotoPaintController : TGPhotoEditorTabController
|
||||
|
||||
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
|
||||
|
||||
- (TGPaintingData *)paintingData;
|
||||
|
||||
+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
|
||||
|
||||
+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
|
||||
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
|
||||
+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
|
||||
+ (CGSize)maximumPaintingSize;
|
||||
|
||||
@end
|
||||
|
||||
extern const CGFloat TGPhotoPaintTopPanelSize;
|
||||
extern const CGFloat TGPhotoPaintBottomPanelSize;
|
||||
//#import "TGPhotoEditorTabController.h"
|
||||
//
|
||||
//#import <LegacyComponents/LegacyComponentsContext.h>
|
||||
//
|
||||
//@class PGPhotoEditor;
|
||||
//@class TGPhotoEditorPreviewView;
|
||||
//
|
||||
//@protocol TGPhotoPaintStickersContext;
|
||||
//
|
||||
//@interface TGPhotoPaintController : TGPhotoEditorTabController
|
||||
//
|
||||
//@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
|
||||
//
|
||||
//- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
|
||||
//
|
||||
//- (TGPaintingData *)paintingData;
|
||||
//
|
||||
//+ (CGRect)photoContainerFrameForParentViewFrame:(CGRect)parentViewFrame toolbarLandscapeSize:(CGFloat)toolbarLandscapeSize orientation:(UIInterfaceOrientation)orientation panelSize:(CGFloat)panelSize hasOnScreenNavigation:(bool)hasOnScreenNavigation;
|
||||
//
|
||||
//+ (CGSize)fittedContentSize:(CGRect)cropRect orientation:(UIImageOrientation)orientation originalSize:(CGSize)originalSize;
|
||||
//+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize keepOriginalSize:(bool)originalSize;
|
||||
//+ (CGPoint)fittedCropRect:(CGRect)cropRect centerScale:(CGFloat)scale;
|
||||
//+ (CGSize)maximumPaintingSize;
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//extern const CGFloat TGPhotoPaintTopPanelSize;
|
||||
//extern const CGFloat TGPhotoPaintBottomPanelSize;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,10 +3,10 @@
|
|||
@class PGPhotoEditor;
|
||||
@class PGPhotoTool;
|
||||
@class TGPhotoEditorPreviewView;
|
||||
@class TGPhotoEntitiesContainerView;
|
||||
@protocol TGPhotoDrawingEntitiesView;
|
||||
|
||||
@interface TGPhotoToolsController : TGPhotoEditorTabController
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView;
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@
|
|||
#import "TGPhotoEditorPreviewView.h"
|
||||
#import "TGPhotoEditorHUDView.h"
|
||||
#import "TGPhotoEditorSparseView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
#import "TGPhotoPaintController.h"
|
||||
#import "TGPhotoDrawingController.h"
|
||||
|
||||
const CGFloat TGPhotoEditorToolsPanelSize = 180.0f;
|
||||
const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize + 40.0f;
|
||||
|
|
@ -48,7 +47,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
|
|||
TGPhotoEditorCollectionView *_portraitCollectionView;
|
||||
TGPhotoEditorCollectionView *_landscapeCollectionView;
|
||||
TGPhotoEditorHUDView *_hudView;
|
||||
TGPhotoEntitiesContainerView *_entitiesView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_entitiesView;
|
||||
|
||||
void (^_changeBlock)(PGPhotoTool *, id, bool);
|
||||
void (^_interactionBegan)(void);
|
||||
|
|
@ -71,7 +70,7 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
|
|||
|
||||
@implementation TGPhotoToolsController
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(TGPhotoEntitiesContainerView *)entitiesView
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context photoEditor:(PGPhotoEditor *)photoEditor previewView:(TGPhotoEditorPreviewView *)previewView entitiesView:(UIView<TGPhotoDrawingEntitiesView> *)entitiesView
|
||||
{
|
||||
self = [super initWithContext:context];
|
||||
if (self != nil)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
@implementation TGPhotoVideoEditor
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed
|
||||
{
|
||||
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
|
||||
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
|
||||
void (^present)(UIImage *) = ^(UIImage *screenImage) {
|
||||
TGPhotoEditorController *controller = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:editableItem intent:TGPhotoEditorControllerAvatarIntent | TGPhotoEditorControllerSuggestedAvatarIntent adjustments:nil caption:nil screenImage:screenImage availableTabs:[TGPhotoEditorController defaultTabsForAvatarIntent] selectedTab:TGPhotoEditorCropTab];
|
||||
controller.senderName = senderName;
|
||||
controller.stickersContext = stickersContext;
|
||||
|
||||
TGMediaAvatarEditorTransition *transition;
|
||||
|
|
|
|||
|
|
@ -76,33 +76,8 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
|
|||
}
|
||||
if (dictionary[@"originalSize"])
|
||||
adjustments->_originalSize = [dictionary[@"originalSize"] CGSizeValue];
|
||||
if (dictionary[@"entities"]) {
|
||||
NSMutableArray *entities = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSDictionary *dict in dictionary[@"entities"]) {
|
||||
if ([dict[@"type"] isEqualToString:@"sticker"]) {
|
||||
TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:dict[@"data"] baseSize:[dict[@"baseSize"] CGSizeValue] animated:[dict[@"animated"] boolValue]];
|
||||
entity.uuid = [dict[@"uuid"] integerValue];
|
||||
entity.position = [dict[@"position"] CGPointValue];
|
||||
entity.scale = [dict[@"scale"] floatValue];
|
||||
entity.angle = [dict[@"angle"] floatValue];
|
||||
entity.mirrored = [dict[@"mirrored"] boolValue];
|
||||
[entities addObject:entity];
|
||||
} else if ([dict[@"type"] isEqualToString:@"text"]) {
|
||||
UIImage *renderImage = [[UIImage alloc] initWithData:dict[@"data"]];
|
||||
if (renderImage != nil) {
|
||||
TGPhotoPaintTextEntity *entity = [[TGPhotoPaintTextEntity alloc] initWithText:nil font:nil swatch:nil baseFontSize:0.0 maxWidth:0.0 style:TGPhotoPaintTextEntityStyleRegular];
|
||||
entity.uuid = [dict[@"uuid"] integerValue];
|
||||
entity.position = [dict[@"position"] CGPointValue];
|
||||
entity.scale = [dict[@"scale"] floatValue];
|
||||
entity.angle = [dict[@"angle"] floatValue];
|
||||
entity.renderImage = renderImage;
|
||||
[entities addObject:entity];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entities:entities];
|
||||
if (dictionary[@"entitiesData"]) {
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"] entitiesData:dictionary[@"entitiesData"]];
|
||||
} else if (dictionary[@"paintingImagePath"]) {
|
||||
adjustments->_paintingData = [TGPaintingData dataWithPaintingImagePath:dictionary[@"paintingImagePath"]];
|
||||
}
|
||||
|
|
@ -240,42 +215,8 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5;
|
|||
if (self.paintingData.imagePath != nil) {
|
||||
dict[@"paintingImagePath"] = self.paintingData.imagePath;
|
||||
}
|
||||
|
||||
NSMutableArray *entities = [[NSMutableArray alloc] init];
|
||||
|
||||
if (self.paintingData.entities != nil) {
|
||||
for (TGPhotoPaintEntity *entity in self.paintingData.entities) {
|
||||
if ([entity isKindOfClass:[TGPhotoPaintStickerEntity class]]) {
|
||||
TGPhotoPaintStickerEntity *stickerEntity = (TGPhotoPaintStickerEntity *)entity;
|
||||
NSMutableDictionary *sticker = [[NSMutableDictionary alloc] init];
|
||||
sticker[@"type"] = @"sticker";
|
||||
sticker[@"baseSize"] = [NSValue valueWithCGSize:stickerEntity.baseSize];
|
||||
sticker[@"uuid"] = @(stickerEntity.uuid);
|
||||
sticker[@"data"] = stickerEntity.document;
|
||||
sticker[@"position"] = [NSValue valueWithCGPoint:stickerEntity.position];
|
||||
sticker[@"scale"] = @(stickerEntity.scale);
|
||||
sticker[@"angle"] = @(stickerEntity.angle);
|
||||
sticker[@"mirrored"] = @(stickerEntity.mirrored);
|
||||
sticker[@"animated"] = @(stickerEntity.animated);
|
||||
[entities addObject:sticker];
|
||||
} else if ([entity isKindOfClass:[TGPhotoPaintTextEntity class]]) {
|
||||
TGPhotoPaintTextEntity *textEntity = (TGPhotoPaintTextEntity *)entity;
|
||||
NSMutableDictionary *text = [[NSMutableDictionary alloc] init];
|
||||
if (textEntity.renderImage != nil) {
|
||||
text[@"type"] = @"text";
|
||||
text[@"uuid"] = @(textEntity.uuid);
|
||||
text[@"data"] = UIImagePNGRepresentation(textEntity.renderImage);
|
||||
text[@"position"] = [NSValue valueWithCGPoint:textEntity.position];
|
||||
text[@"scale"] = @(textEntity.scale);
|
||||
text[@"angle"] = @(textEntity.angle);
|
||||
[entities addObject:text];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entities.count > 0) {
|
||||
dict[@"entities"] = entities;
|
||||
if (self.paintingData.entitiesData != nil) {
|
||||
dict[@"entitiesData"] = self.paintingData.entitiesData;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, t
|
|||
}
|
||||
|
||||
|
||||
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
|
||||
public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, senderName: String? = nil, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) {
|
||||
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media)
|
||||
|> deliverOnMainQueue).start(next: { (value, isImage) in
|
||||
guard case let .data(data) = value, data.complete else {
|
||||
|
|
@ -93,7 +93,7 @@ public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference
|
|||
|
||||
present(legacyController, nil)
|
||||
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, didFinishWithImage: { image in
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: url, stickersContext: paintStickersContext, transitionView: transitionView, senderName: senderName, didFinishWithImage: { image in
|
||||
if let image = image {
|
||||
imageCompletion(image)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,18 +290,18 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
|
|||
self.originalSize = adjustments.originalSize
|
||||
self.cropRect = adjustments.cropRect.isEmpty ? nil : adjustments.cropRect
|
||||
|
||||
var entities: [LegacyPaintEntity] = []
|
||||
if let paintingData = adjustments.paintingData, let paintingEntities = paintingData.entities {
|
||||
for paintingEntity in paintingEntities {
|
||||
if let sticker = paintingEntity as? TGPhotoPaintStickerEntity {
|
||||
if let account = account, let entity = LegacyPaintStickerEntity(account: account, entity: sticker) {
|
||||
entities.append(entity)
|
||||
}
|
||||
} else if let text = paintingEntity as? TGPhotoPaintTextEntity {
|
||||
entities.append(LegacyPaintTextEntity(entity: text))
|
||||
}
|
||||
}
|
||||
}
|
||||
let entities: [LegacyPaintEntity] = []
|
||||
// if let paintingData = adjustments.paintingData, let paintingEntities = paintingData.entities {
|
||||
// for paintingEntity in paintingEntities {
|
||||
// if let sticker = paintingEntity as? TGPhotoPaintStickerEntity {
|
||||
// if let account = account, let entity = LegacyPaintStickerEntity(account: account, entity: sticker) {
|
||||
// entities.append(entity)
|
||||
// }
|
||||
// } else if let text = paintingEntity as? TGPhotoPaintTextEntity {
|
||||
// entities.append(LegacyPaintTextEntity(entity: text))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
self.entities = entities
|
||||
|
||||
super.init()
|
||||
|
|
@ -482,8 +482,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
|||
let contentWrapperView: UIView!
|
||||
let interfaceController: TGPhotoDrawingInterfaceController!
|
||||
|
||||
init(context: AccountContext, size: CGSize) {
|
||||
let interfaceController = DrawingScreen(context: context, size: size)
|
||||
init(context: AccountContext, size: CGSize, originalSize: CGSize) {
|
||||
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize)
|
||||
self.interfaceController = interfaceController
|
||||
self.drawingView = interfaceController.drawingView
|
||||
self.drawingEntitiesView = interfaceController.entitiesView
|
||||
|
|
@ -494,8 +494,8 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
|||
}
|
||||
}
|
||||
|
||||
public func drawingAdapter(_ size: CGSize) -> TGPhotoDrawingAdapter! {
|
||||
return LegacyDrawingAdapter(context: self.context, size: size)
|
||||
public func drawingAdapter(_ size: CGSize, originalSize: CGSize) -> TGPhotoDrawingAdapter! {
|
||||
return LegacyDrawingAdapter(context: self.context, size: size, originalSize: originalSize)
|
||||
}
|
||||
|
||||
public func solidRoundedButton(_ title: String!, action: (() -> Void)!) -> (UIView & TGPhotoSolidRoundedButtonView)! {
|
||||
|
|
@ -504,6 +504,11 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
|||
button.pressed = action
|
||||
return button
|
||||
}
|
||||
|
||||
public func drawingEntitiesView(with size: CGSize) -> (UIView & TGPhotoDrawingEntitiesView)! {
|
||||
let view = DrawingEntitiesView(context: self.context, size: size)
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
extension SolidRoundedButtonView: TGPhotoSolidRoundedButtonView {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public extension EnginePeer {
|
|||
} else if let lastName = user.lastName, !lastName.isEmpty {
|
||||
return lastName
|
||||
} else if let _ = user.phone {
|
||||
return ""// formatPhoneNumber("+\(phone)")
|
||||
return "" //formatPhoneNumber("+\(phone)")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ public extension EnginePeer {
|
|||
} else if let lastName = user.lastName, !lastName.isEmpty {
|
||||
return lastName
|
||||
} else if let _ = user.phone {
|
||||
return ""//formatPhoneNumber("+\(phone)")
|
||||
return "" //formatPhoneNumber("+\(phone)")
|
||||
} else {
|
||||
return strings.User_DeletedAccount
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1355,13 +1355,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
}
|
||||
|
||||
if let undoOverlayController = strongSelf.undoOverlayController {
|
||||
undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, undo: true)
|
||||
undoOverlayController.content = .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true)
|
||||
} else {
|
||||
var elevatedLayout = true
|
||||
if let layout = strongSelf.validLayout, case .regular = layout.metrics.widthClass {
|
||||
elevatedLayout = false
|
||||
}
|
||||
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in
|
||||
let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image ?? UIImage(), title: nil, text: text, round: false, undo: true), elevatedLayout: elevatedLayout, action: { [weak self] action in
|
||||
guard let strongSelf = self else {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,6 +364,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
private let context: AccountContext
|
||||
private let peer: Peer
|
||||
private let sourceCorners: SourceCorners
|
||||
private let isSuggested: Bool
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
|
|
@ -403,10 +404,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
|
||||
private let editDisposable = MetaDisposable ()
|
||||
|
||||
public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, synchronousLoad: Bool = false) {
|
||||
public init(context: AccountContext, peer: Peer, sourceCorners: SourceCorners = .round, remoteEntries: Promise<[AvatarGalleryEntry]>? = nil, isSuggested: Bool = false, skipInitial: Bool = false, centralEntryIndex: Int? = nil, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, synchronousLoad: Bool = false) {
|
||||
self.context = context
|
||||
self.peer = peer
|
||||
self.sourceCorners = sourceCorners
|
||||
self.isSuggested = isSuggested
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.replaceRootController = replaceRootController
|
||||
|
||||
|
|
@ -460,6 +462,11 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
|
|||
if strongSelf.centralEntryIndex == nil {
|
||||
strongSelf.centralEntryIndex = 0
|
||||
}
|
||||
|
||||
if strongSelf.isSuggested, let firstEntry = entries.first {
|
||||
strongSelf.navigationItem.title = !firstEntry.videoRepresentations.isEmpty ? strongSelf.presentationData.strings.Conversation_SuggestedVideoTitle : strongSelf.presentationData.strings.Conversation_SuggestedPhotoTitle
|
||||
}
|
||||
|
||||
if strongSelf.isViewLoaded {
|
||||
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ entry in PeerAvatarImageGalleryItem(context: context, peer: peer, presentationData: presentationData, entry: entry, sourceCorners: sourceCorners, delete: strongSelf.canDelete ? {
|
||||
self?.deleteEntry(entry)
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
var highlightedSide: Bool?
|
||||
public let stripContainerNode: ASDisplayNode
|
||||
public let highlightContainerNode: ASDisplayNode
|
||||
private let setByYouNode: ImmediateTextNode
|
||||
public let setByYouNode: ImmediateTextNode
|
||||
private let setByYouImageNode: ImageNode
|
||||
private var setByYouTapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
|
|
@ -883,7 +883,7 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode {
|
|||
self.currentIndexUpdated?()
|
||||
}
|
||||
if let size = self.validLayout {
|
||||
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring))
|
||||
self.updateItems(size: size, transition: .immediate, stripTransition: .animated(duration: 0.3, curve: .spring), synchronous: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,13 +152,8 @@ public final class QrCodeScanScreen: ViewController {
|
|||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.completion(nil)
|
||||
self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
self.dismiss()
|
||||
})
|
||||
self.dismissAnimated()
|
||||
}
|
||||
|
||||
@objc private func myCodePressed() {
|
||||
|
|
@ -210,12 +205,20 @@ public final class QrCodeScanScreen: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.controllerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: layout.size.height), duration: 0.2, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
self.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
private func completeWithCode(_ code: String) {
|
||||
guard case .custom = self.subject else {
|
||||
return
|
||||
}
|
||||
self.completion(code)
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
|
|
|
|||
|
|
@ -152,185 +152,3 @@ public func ChangePhoneNumberController(context: AccountContext) -> ViewControll
|
|||
|
||||
return controller
|
||||
}
|
||||
|
||||
//public final class ChangePhoneNumberController: ViewController, MFMailComposeViewControllerDelegate {
|
||||
// private var controllerNode: ChangePhoneNumberControllerNode {
|
||||
// return self.displayNode as! ChangePhoneNumberControllerNode
|
||||
// }
|
||||
//
|
||||
// private let context: AccountContext
|
||||
//
|
||||
// private var currentData: (Int32, String?, String)?
|
||||
// private let requestDisposable = MetaDisposable()
|
||||
//
|
||||
// var inProgress: Bool = false {
|
||||
// didSet {
|
||||
// if self.inProgress {
|
||||
// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor))
|
||||
// self.navigationItem.rightBarButtonItem = item
|
||||
// } else {
|
||||
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
|
||||
// }
|
||||
// self.controllerNode.inProgress = self.inProgress
|
||||
// }
|
||||
// }
|
||||
// var loginWithNumber: ((String) -> Void)?
|
||||
//
|
||||
// private let hapticFeedback = HapticFeedback()
|
||||
//
|
||||
// private var presentationData: PresentationData
|
||||
//
|
||||
// public init(context: AccountContext) {
|
||||
// self.context = context
|
||||
// self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//
|
||||
// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
//
|
||||
// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
// self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
//
|
||||
// self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title
|
||||
// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed))
|
||||
// self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
// }
|
||||
//
|
||||
// required init(coder aDecoder: NSCoder) {
|
||||
// fatalError("init(coder:) has not been implemented")
|
||||
// }
|
||||
//
|
||||
// deinit {
|
||||
// self.requestDisposable.dispose()
|
||||
// }
|
||||
//
|
||||
// func updateData(countryCode: Int32, countryName: String, number: String) {
|
||||
// if self.currentData == nil || self.currentData! != (countryCode, countryName, number) {
|
||||
// self.currentData = (countryCode, countryName, number)
|
||||
// if self.isNodeLoaded {
|
||||
// self.controllerNode.codeAndNumber = (countryCode, countryName, number)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override public func loadDisplayNode() {
|
||||
// self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData)
|
||||
// self.displayNodeDidLoad()
|
||||
// self.controllerNode.selectCountryCode = { [weak self] in
|
||||
// if let strongSelf = self {
|
||||
// let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme)
|
||||
// controller.completeWithCountryCode = { code, name in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.updateData(countryCode: Int32(code), countryName: name, number: strongSelf.controllerNode.codeAndNumber.2)
|
||||
// strongSelf.controllerNode.activateInput()
|
||||
// }
|
||||
// }
|
||||
// strongSelf.controllerNode.view.endEditing(true)
|
||||
// strongSelf.push(controller)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// loadServerCountryCodes(accountManager: self.context.sharedContext.accountManager, engine: self.context.engine, completion: { [weak self] in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.controllerNode.updateCountryCode()
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
||||
// }
|
||||
//
|
||||
// override public func viewWillAppear(_ animated: Bool) {
|
||||
// super.viewWillAppear(animated)
|
||||
//
|
||||
// self.controllerNode.activateInput()
|
||||
// }
|
||||
//
|
||||
// override public func viewDidAppear(_ animated: Bool) {
|
||||
// super.viewDidAppear(animated)
|
||||
//
|
||||
// self.controllerNode.activateInput()
|
||||
// }
|
||||
//
|
||||
// override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
// super.containerLayoutUpdated(layout, transition: transition)
|
||||
//
|
||||
// self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
|
||||
// }
|
||||
//
|
||||
// @objc func nextPressed() {
|
||||
// let (code, _, number) = self.controllerNode.codeAndNumber
|
||||
// var phoneNumber = number
|
||||
// if let code = code {
|
||||
// phoneNumber = "\(code)\(phoneNumber)"
|
||||
// }
|
||||
// if !number.isEmpty {
|
||||
// self.inProgress = true
|
||||
// self.requestDisposable.set((self.context.engine.accountData.requestChangeAccountPhoneNumberVerification(phoneNumber: self.controllerNode.currentNumber) |> deliverOnMainQueue).start(next: { [weak self] next in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.inProgress = false
|
||||
// (strongSelf.navigationController as? NavigationController)?.pushViewController(changePhoneNumberCodeController(context: strongSelf.context, phoneNumber: strongSelf.controllerNode.currentNumber, codeData: next))
|
||||
// }
|
||||
// }, error: { [weak self] error in
|
||||
// if let strongSelf = self {
|
||||
// strongSelf.inProgress = false
|
||||
//
|
||||
// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
//
|
||||
// let text: String
|
||||
// var actions: [TextAlertAction] = []
|
||||
// switch error {
|
||||
// case .limitExceeded:
|
||||
// text = presentationData.strings.Login_CodeFloodError
|
||||
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
// case .invalidPhoneNumber:
|
||||
// text = presentationData.strings.Login_InvalidPhoneError
|
||||
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
// case .phoneNumberOccupied:
|
||||
// text = presentationData.strings.ChangePhone_ErrorOccupied(formatPhoneNumber(context: strongSelf.context, number: phoneNumber)).string
|
||||
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
// case .phoneBanned:
|
||||
// text = presentationData.strings.Login_PhoneBannedError
|
||||
// actions.append(TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
// actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
|
||||
// guard let strongSelf = self else {
|
||||
// return
|
||||
// }
|
||||
// let formattedNumber = formatPhoneNumber(context: strongSelf.context, number: number)
|
||||
// let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
// let systemVersion = UIDevice.current.systemVersion
|
||||
// let locale = Locale.current.identifier
|
||||
// let carrier = CTCarrier()
|
||||
// let mnc = carrier.mobileNetworkCode ?? "none"
|
||||
//
|
||||
// strongSelf.presentEmailComposeController(address: "login@stel.com", subject: presentationData.strings.Login_PhoneBannedEmailSubject(formattedNumber).string, body: presentationData.strings.Login_PhoneBannedEmailBody(formattedNumber, appVersion, systemVersion, locale, mnc).string)
|
||||
// }))
|
||||
// case .generic:
|
||||
// text = presentationData.strings.Login_UnknownError
|
||||
// actions.append(TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}))
|
||||
// }
|
||||
//
|
||||
// strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: actions), in: .window(.root))
|
||||
// }
|
||||
// }))
|
||||
// } else {
|
||||
// self.hapticFeedback.error()
|
||||
// self.controllerNode.animateError()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func presentEmailComposeController(address: String, subject: String, body: String) {
|
||||
// if MFMailComposeViewController.canSendMail() {
|
||||
// let composeController = MFMailComposeViewController()
|
||||
// composeController.setToRecipients([address])
|
||||
// composeController.setSubject(subject)
|
||||
// composeController.setMessageBody(body, isHTML: false)
|
||||
// composeController.mailComposeDelegate = self
|
||||
//
|
||||
// self.view.window?.rootViewController?.present(composeController, animated: true, completion: nil)
|
||||
// } else {
|
||||
// self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_EmailNotConfiguredError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
// controller.dismiss(animated: true, completion: nil)
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ public enum PrivacyIntroControllerMode {
|
|||
}
|
||||
}
|
||||
|
||||
func title(strings: PresentationStrings) -> String {
|
||||
func title(context: AccountContext, strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .passcode:
|
||||
return strings.PasscodeSettings_Title
|
||||
case .twoStepVerification:
|
||||
return strings.TwoStepAuth_AdditionalPassword
|
||||
case let .changePhoneNumber(phoneNumber):
|
||||
return formatPhoneNumber(phoneNumber)
|
||||
return formatPhoneNumber(context: context, number: phoneNumber)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ final class PrivacyIntroControllerNode: ViewControllerTracingNode {
|
|||
if self.animationNode.isHidden {
|
||||
self.iconNode.image = self.mode.icon(theme: presentationData.theme)
|
||||
}
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.mode.title(context: self.context, strings: presentationData.strings), font: titleFont, textColor: presentationData.theme.list.sectionHeaderTextColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.mode.text(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
self.noticeNode.attributedText = NSAttributedString(string: self.mode.notice(strings: presentationData.strings), font: textFont, textColor: presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
|
||||
self.buttonTextNode.attributedText = NSAttributedString(string: self.mode.buttonTitle(strings: presentationData.strings), font: buttonFont, textColor: presentationData.theme.list.itemAccentColor, paragraphAlignment: .center)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import PresentationDataUtils
|
|||
import AccountContext
|
||||
import UndoUI
|
||||
import ItemListPeerActionItem
|
||||
import AvatarNode
|
||||
|
||||
enum SelectivePrivacySettingsKind {
|
||||
case presence
|
||||
|
|
@ -414,8 +415,8 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry {
|
|||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||
arguments.setPublicPhoto?()
|
||||
})
|
||||
case let .removePublicPhoto(theme, text, _, _):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
|
||||
case let .removePublicPhoto(_, text, peer, image):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: nil, iconSignal: peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
|
||||
arguments.removePublicPhoto?()
|
||||
})
|
||||
case let .publicPhotoInfo(_, text):
|
||||
|
|
@ -676,10 +677,10 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
|
|||
entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
|
||||
}
|
||||
|
||||
if case .profilePhoto = kind, let peer = peer {
|
||||
if case .profilePhoto = kind, let peer = peer, state.setting != .everybody {
|
||||
if let publicPhoto = publicPhoto {
|
||||
entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_UpdatePublicPhoto))
|
||||
entries.append(.removePublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_RemovePublicPhoto, peer, publicPhoto))
|
||||
entries.append(.removePublicPhoto(presentationData.theme, !publicPhoto.videoRepresentations.isEmpty ? presentationData.strings.Privacy_ProfilePhoto_RemovePublicVideo : presentationData.strings.Privacy_ProfilePhoto_RemovePublicPhoto, peer, publicPhoto))
|
||||
} else {
|
||||
entries.append(.setPublicPhoto(presentationData.theme, presentationData.strings.Privacy_ProfilePhoto_SetPublicPhoto))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -699,13 +699,6 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
|
|||
return entries
|
||||
}
|
||||
|
||||
public enum InstalledStickerPacksControllerMode {
|
||||
case general
|
||||
case modal
|
||||
case masks
|
||||
case emoji
|
||||
}
|
||||
|
||||
public func installedStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, archivedPacks: [ArchivedStickerPackItem]? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void = { _ in }, focusOnItemTag: InstalledStickerPacksEntryTag? = nil) -> ViewController {
|
||||
let initialState = InstalledStickerPacksControllerState().withUpdatedEditing(mode == .modal).withUpdatedSelectedPackIds(mode == .modal ? Set() : nil)
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@ final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode {
|
|||
|
||||
return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue()))
|
||||
|> then(
|
||||
requestContextResults(context: context, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16)
|
||||
requestContextResults(engine: context.engine, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16)
|
||||
|> map { results -> ChatContextResultCollection? in
|
||||
return results?.results
|
||||
}
|
||||
|
|
|
|||
|
|
@ -535,3 +535,48 @@ extension ChatContextResultCollection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func requestContextResults(engine: TelegramEngine, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal<RequestChatContextResultsResult?, NoError> {
|
||||
return engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|
||||
|> `catch` { error -> Signal<RequestChatContextResultsResult?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { resultsStruct -> Signal<RequestChatContextResultsResult?, NoError> in
|
||||
let results = resultsStruct?.results
|
||||
|
||||
var collection = existingResults
|
||||
var updated: Bool = false
|
||||
if let existingResults = existingResults, let results = results {
|
||||
var newResults: [ChatContextResult] = []
|
||||
var existingIds = Set<String>()
|
||||
for result in existingResults.results {
|
||||
newResults.append(result)
|
||||
existingIds.insert(result.id)
|
||||
}
|
||||
for result in results.results {
|
||||
if !existingIds.contains(result.id) {
|
||||
newResults.append(result)
|
||||
existingIds.insert(result.id)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, results: newResults, cacheTimeout: existingResults.cacheTimeout)
|
||||
} else {
|
||||
collection = results
|
||||
updated = true
|
||||
}
|
||||
if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated {
|
||||
let nextResults = requestContextResults(engine: engine, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit)
|
||||
if collection.results.count > 10 {
|
||||
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
|
||||
|> then(nextResults)
|
||||
} else {
|
||||
return nextResults
|
||||
}
|
||||
} else if let collection = collection {
|
||||
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -337,6 +337,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent:EmojiStatusSelectionComponent",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
||||
"//submodules/Media/LocalAudioTranscription:LocalAudioTranscription",
|
||||
|
|
@ -348,8 +349,11 @@ swift_library(
|
|||
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode",
|
||||
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
|
||||
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
|
||||
"//submodules/DrawingUI:DrawingUI",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_armv7": [],
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatControllerInteraction",
|
||||
module_name = "ChatControllerInteraction",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TextSelectionNode:TextSelectionNode",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -15,33 +15,28 @@ import UndoUI
|
|||
import TelegramPresentationData
|
||||
import ChatPresentationInterfaceState
|
||||
import TextFormat
|
||||
import WallpaperBackgroundNode
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
struct ChatInterfaceHighlightedState: Equatable {
|
||||
let messageStableId: UInt32
|
||||
public struct ChatInterfaceHighlightedState: Equatable {
|
||||
public let messageStableId: UInt32
|
||||
|
||||
static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool {
|
||||
public init(messageStableId: UInt32) {
|
||||
self.messageStableId = messageStableId
|
||||
}
|
||||
|
||||
public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool {
|
||||
return lhs.messageStableId == rhs.messageStableId
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfaceStickerSettings: Equatable {
|
||||
let loopAnimatedStickers: Bool
|
||||
public struct ChatInterfacePollActionState: Equatable {
|
||||
public var pollMessageIdsInProgress: [MessageId: [Data]] = [:]
|
||||
|
||||
public init(loopAnimatedStickers: Bool) {
|
||||
self.loopAnimatedStickers = loopAnimatedStickers
|
||||
public init(pollMessageIdsInProgress: [MessageId: [Data]] = [:]) {
|
||||
self.pollMessageIdsInProgress = pollMessageIdsInProgress
|
||||
}
|
||||
|
||||
public init(stickerSettings: StickerSettings) {
|
||||
self.loopAnimatedStickers = stickerSettings.loopAnimatedStickers
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatInterfaceStickerSettings, rhs: ChatInterfaceStickerSettings) -> Bool {
|
||||
return lhs.loopAnimatedStickers == rhs.loopAnimatedStickers
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfacePollActionState: Equatable {
|
||||
var pollMessageIdsInProgress: [MessageId: [Data]] = [:]
|
||||
}
|
||||
|
||||
public enum ChatControllerInteractionSwipeAction {
|
||||
|
|
@ -54,128 +49,150 @@ public enum ChatControllerInteractionReaction {
|
|||
case reaction(MessageReaction.Reaction)
|
||||
}
|
||||
|
||||
struct UnreadMessageRangeKey: Hashable {
|
||||
var peerId: PeerId
|
||||
var namespace: MessageId.Namespace
|
||||
public struct UnreadMessageRangeKey: Hashable {
|
||||
public var peerId: PeerId
|
||||
public var namespace: MessageId.Namespace
|
||||
|
||||
public init(peerId: PeerId, namespace: MessageId.Namespace) {
|
||||
self.peerId = peerId
|
||||
self.namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatPresentationContext {
|
||||
public weak var backgroundNode: WallpaperBackgroundNode?
|
||||
public let animationCache: AnimationCache
|
||||
public let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
public init(context: AccountContext, backgroundNode: WallpaperBackgroundNode?) {
|
||||
self.backgroundNode = backgroundNode
|
||||
|
||||
self.animationCache = context.animationCache
|
||||
self.animationRenderer = context.animationRenderer
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ChatMessageTransitionProtocol: ASDisplayNode {
|
||||
|
||||
}
|
||||
|
||||
public final class ChatControllerInteraction {
|
||||
enum OpenPeerSource {
|
||||
public enum OpenPeerSource {
|
||||
case `default`
|
||||
case reaction
|
||||
case groupParticipant
|
||||
}
|
||||
|
||||
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
||||
let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
|
||||
let openPeerMention: (String) -> Void
|
||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
|
||||
let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
|
||||
let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
|
||||
let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
||||
let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
let navigateToMessage: (MessageId, MessageId) -> Void
|
||||
let navigateToMessageStandalone: (MessageId) -> Void
|
||||
let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
|
||||
let tapMessage: ((Message) -> Void)?
|
||||
let clickThroughMessage: () -> Void
|
||||
let toggleMessagesSelection: ([MessageId], Bool) -> Void
|
||||
let sendCurrentMessage: (Bool) -> Void
|
||||
let sendMessage: (String) -> Void
|
||||
let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
|
||||
let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void
|
||||
let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
|
||||
let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool
|
||||
let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
|
||||
let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
|
||||
let activateSwitchInline: (PeerId?, String) -> Void
|
||||
let openUrl: (String, Bool, Bool?, Message?) -> Void
|
||||
let shareCurrentLocation: () -> Void
|
||||
let shareAccountContact: () -> Void
|
||||
let sendBotCommand: (MessageId?, String) -> Void
|
||||
let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void
|
||||
let openWallpaper: (Message) -> Void
|
||||
let openTheme: (Message) -> Void
|
||||
let openHashtag: (String?, String) -> Void
|
||||
let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void
|
||||
let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void
|
||||
let openMessageShareMenu: (MessageId) -> Void
|
||||
let presentController: (ViewController, Any?) -> Void
|
||||
let presentControllerInCurrent: (ViewController, Any?) -> Void
|
||||
let navigationController: () -> NavigationController?
|
||||
let chatControllerNode: () -> ASDisplayNode?
|
||||
let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
let callPeer: (PeerId, Bool) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
|
||||
let openCheckoutOrReceipt: (MessageId) -> Void
|
||||
let openSearch: () -> Void
|
||||
let setupReply: (MessageId) -> Void
|
||||
let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction
|
||||
let navigateToFirstDateMessage: (Int32, Bool) -> Void
|
||||
let requestRedeliveryOfFailedMessages: (MessageId) -> Void
|
||||
let addContact: (String) -> Void
|
||||
let rateCall: (Message, CallId, Bool) -> Void
|
||||
let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void
|
||||
let requestOpenMessagePollResults: (MessageId, MediaId) -> Void
|
||||
let openAppStorePage: () -> Void
|
||||
let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
|
||||
let seekToTimecode: (Message, Double, Bool) -> Void
|
||||
let scheduleCurrentMessage: () -> Void
|
||||
let sendScheduledMessagesNow: ([MessageId]) -> Void
|
||||
let editScheduledMessagesTime: ([MessageId]) -> Void
|
||||
let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void
|
||||
let displayImportedMessageTooltip: (ASDisplayNode) -> Void
|
||||
let displaySwipeToReplyHint: () -> Void
|
||||
let dismissReplyMarkupMessage: (Message) -> Void
|
||||
let openMessagePollResults: (MessageId, Data) -> Void
|
||||
let openPollCreation: (Bool?) -> Void
|
||||
let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
|
||||
let displayPsa: (String, ASDisplayNode) -> Void
|
||||
let displayDiceTooltip: (TelegramMediaDice) -> Void
|
||||
let animateDiceSuccess: (Bool, Bool) -> Void
|
||||
let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void
|
||||
let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void
|
||||
let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
let openMessageReplies: (MessageId, Bool, Bool) -> Void
|
||||
let openReplyThreadOriginalMessage: (Message) -> Void
|
||||
let openMessageStats: (MessageId) -> Void
|
||||
let editMessageMedia: (MessageId, Bool) -> Void
|
||||
let copyText: (String) -> Void
|
||||
let displayUndo: (UndoOverlayContent) -> Void
|
||||
let isAnimatingMessage: (UInt32) -> Bool
|
||||
let getMessageTransitionNode: () -> ChatMessageTransitionNode?
|
||||
let updateChoosingSticker: (Bool) -> Void
|
||||
let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void
|
||||
let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
||||
let openJoinLink: (String) -> Void
|
||||
let openWebView: (String, String, Bool, Bool) -> Void
|
||||
let activateAdAction: (EngineMessage.Id) -> Void
|
||||
public let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
||||
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
|
||||
public let openPeerMention: (String) -> Void
|
||||
public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
|
||||
public let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
|
||||
public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
|
||||
public let activateMessagePinch: (PinchSourceContainerNode) -> Void
|
||||
public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
public let navigateToMessage: (MessageId, MessageId) -> Void
|
||||
public let navigateToMessageStandalone: (MessageId) -> Void
|
||||
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
|
||||
public let tapMessage: ((Message) -> Void)?
|
||||
public let clickThroughMessage: () -> Void
|
||||
public let toggleMessagesSelection: ([MessageId], Bool) -> Void
|
||||
public let sendCurrentMessage: (Bool) -> Void
|
||||
public let sendMessage: (String) -> Void
|
||||
public let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Bool
|
||||
public let sendEmoji: (String, ChatTextInputTextCustomEmojiAttribute) -> Void
|
||||
public let sendGif: (FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool
|
||||
public let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, UIView, CGRect, Bool) -> Bool
|
||||
public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
|
||||
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
|
||||
public let activateSwitchInline: (PeerId?, String) -> Void
|
||||
public let openUrl: (String, Bool, Bool?, Message?) -> Void
|
||||
public let shareCurrentLocation: () -> Void
|
||||
public let shareAccountContact: () -> Void
|
||||
public let sendBotCommand: (MessageId?, String) -> Void
|
||||
public let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void
|
||||
public let openWallpaper: (Message) -> Void
|
||||
public let openTheme: (Message) -> Void
|
||||
public let openHashtag: (String?, String) -> Void
|
||||
public let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void
|
||||
public let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void
|
||||
public let openMessageShareMenu: (MessageId) -> Void
|
||||
public let presentController: (ViewController, Any?) -> Void
|
||||
public let presentControllerInCurrent: (ViewController, Any?) -> Void
|
||||
public let navigationController: () -> NavigationController?
|
||||
public let chatControllerNode: () -> ASDisplayNode?
|
||||
public let presentGlobalOverlayController: (ViewController, Any?) -> Void
|
||||
public let callPeer: (PeerId, Bool) -> Void
|
||||
public let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
|
||||
public let openCheckoutOrReceipt: (MessageId) -> Void
|
||||
public let openSearch: () -> Void
|
||||
public let setupReply: (MessageId) -> Void
|
||||
public let canSetupReply: (Message) -> ChatControllerInteractionSwipeAction
|
||||
public let navigateToFirstDateMessage: (Int32, Bool) -> Void
|
||||
public let requestRedeliveryOfFailedMessages: (MessageId) -> Void
|
||||
public let addContact: (String) -> Void
|
||||
public let rateCall: (Message, CallId, Bool) -> Void
|
||||
public let requestSelectMessagePollOptions: (MessageId, [Data]) -> Void
|
||||
public let requestOpenMessagePollResults: (MessageId, MediaId) -> Void
|
||||
public let openAppStorePage: () -> Void
|
||||
public let displayMessageTooltip: (MessageId, String, ASDisplayNode?, CGRect?) -> Void
|
||||
public let seekToTimecode: (Message, Double, Bool) -> Void
|
||||
public let scheduleCurrentMessage: () -> Void
|
||||
public let sendScheduledMessagesNow: ([MessageId]) -> Void
|
||||
public let editScheduledMessagesTime: ([MessageId]) -> Void
|
||||
public let performTextSelectionAction: (Bool, NSAttributedString, TextSelectionAction) -> Void
|
||||
public let displayImportedMessageTooltip: (ASDisplayNode) -> Void
|
||||
public let displaySwipeToReplyHint: () -> Void
|
||||
public let dismissReplyMarkupMessage: (Message) -> Void
|
||||
public let openMessagePollResults: (MessageId, Data) -> Void
|
||||
public let openPollCreation: (Bool?) -> Void
|
||||
public let displayPollSolution: (TelegramMediaPollResults.Solution, ASDisplayNode) -> Void
|
||||
public let displayPsa: (String, ASDisplayNode) -> Void
|
||||
public let displayDiceTooltip: (TelegramMediaDice) -> Void
|
||||
public let animateDiceSuccess: (Bool, Bool) -> Void
|
||||
public let displayPremiumStickerTooltip: (TelegramMediaFile, Message) -> Void
|
||||
public let displayEmojiPackTooltip: (TelegramMediaFile, Message) -> Void
|
||||
public let openPeerContextMenu: (Peer, MessageId?, ASDisplayNode, CGRect, ContextGesture?) -> Void
|
||||
public let openMessageReplies: (MessageId, Bool, Bool) -> Void
|
||||
public let openReplyThreadOriginalMessage: (Message) -> Void
|
||||
public let openMessageStats: (MessageId) -> Void
|
||||
public let editMessageMedia: (MessageId, Bool) -> Void
|
||||
public let copyText: (String) -> Void
|
||||
public let displayUndo: (UndoOverlayContent) -> Void
|
||||
public let isAnimatingMessage: (UInt32) -> Bool
|
||||
public let getMessageTransitionNode: () -> ChatMessageTransitionProtocol?
|
||||
public let updateChoosingSticker: (Bool) -> Void
|
||||
public let commitEmojiInteraction: (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void
|
||||
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
|
||||
public let openJoinLink: (String) -> Void
|
||||
public let openWebView: (String, String, Bool, Bool) -> Void
|
||||
public let activateAdAction: (EngineMessage.Id) -> Void
|
||||
|
||||
let requestMessageUpdate: (MessageId, Bool) -> Void
|
||||
let cancelInteractiveKeyboardGestures: () -> Void
|
||||
let dismissTextInput: () -> Void
|
||||
let scrollToMessageId: (MessageIndex) -> Void
|
||||
public let requestMessageUpdate: (MessageId, Bool) -> Void
|
||||
public let cancelInteractiveKeyboardGestures: () -> Void
|
||||
public let dismissTextInput: () -> Void
|
||||
public let scrollToMessageId: (MessageIndex) -> Void
|
||||
|
||||
var canPlayMedia: Bool = false
|
||||
var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
var expandedTranslationMessageStableIds: Set<UInt32> = Set()
|
||||
var selectionState: ChatInterfaceSelectionState?
|
||||
var highlightedState: ChatInterfaceHighlightedState?
|
||||
var contextHighlightedState: ChatInterfaceHighlightedState?
|
||||
var automaticMediaDownloadSettings: MediaAutoDownloadSettings
|
||||
var pollActionState: ChatInterfacePollActionState
|
||||
var currentPollMessageWithTooltip: MessageId?
|
||||
var currentPsaMessageWithTooltip: MessageId?
|
||||
var stickerSettings: ChatInterfaceStickerSettings
|
||||
var searchTextHighightState: (String, [MessageIndex])?
|
||||
var unreadMessageRange: [UnreadMessageRangeKey: Range<MessageId.Id>] = [:]
|
||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
var currentMessageWithLoadingReplyThread: MessageId?
|
||||
var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
let presentationContext: ChatPresentationContext
|
||||
var playNextOutgoingGift: Bool = false
|
||||
public var canPlayMedia: Bool = false
|
||||
public var hiddenMedia: [MessageId: [Media]] = [:]
|
||||
public var expandedTranslationMessageStableIds: Set<UInt32> = Set()
|
||||
public var selectionState: ChatInterfaceSelectionState?
|
||||
public var highlightedState: ChatInterfaceHighlightedState?
|
||||
public var contextHighlightedState: ChatInterfaceHighlightedState?
|
||||
public var automaticMediaDownloadSettings: MediaAutoDownloadSettings
|
||||
public var pollActionState: ChatInterfacePollActionState
|
||||
public var currentPollMessageWithTooltip: MessageId?
|
||||
public var currentPsaMessageWithTooltip: MessageId?
|
||||
public var stickerSettings: ChatInterfaceStickerSettings
|
||||
public var searchTextHighightState: (String, [MessageIndex])?
|
||||
public var unreadMessageRange: [UnreadMessageRangeKey: Range<MessageId.Id>] = [:]
|
||||
public var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||
public var currentMessageWithLoadingReplyThread: MessageId?
|
||||
public var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
public let presentationContext: ChatPresentationContext
|
||||
public var playNextOutgoingGift: Bool = false
|
||||
|
||||
init(
|
||||
public init(
|
||||
openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool,
|
||||
openPeer: @escaping (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void,
|
||||
openPeerMention: @escaping (String) -> Void,
|
||||
|
|
@ -253,7 +270,7 @@ public final class ChatControllerInteraction {
|
|||
copyText: @escaping (String) -> Void,
|
||||
displayUndo: @escaping (UndoOverlayContent) -> Void,
|
||||
isAnimatingMessage: @escaping (UInt32) -> Bool,
|
||||
getMessageTransitionNode: @escaping () -> ChatMessageTransitionNode?,
|
||||
getMessageTransitionNode: @escaping () -> ChatMessageTransitionProtocol?,
|
||||
updateChoosingSticker: @escaping (Bool) -> Void,
|
||||
commitEmojiInteraction: @escaping (MessageId, String, EmojiInteraction, TelegramMediaFile) -> Void,
|
||||
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatEntityKeyboardInputNode",
|
||||
module_name = "ChatEntityKeyboardInputNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -12,7 +12,6 @@ import MultiAnimationRenderer
|
|||
import Postbox
|
||||
import TelegramCore
|
||||
import ComponentDisplayAdapters
|
||||
import SettingsUI
|
||||
import TextFormat
|
||||
import PagerComponent
|
||||
import AppBundle
|
||||
|
|
@ -25,17 +24,37 @@ import AttachmentTextInputPanelNode
|
|||
import TelegramPresentationData
|
||||
import TelegramNotices
|
||||
import StickerPeekUI
|
||||
import ChatInputNode
|
||||
import TelegramUIPreferences
|
||||
import MultiplexedVideoNode
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
|
||||
final class EntityKeyboardGifContent: Equatable {
|
||||
let hasRecentGifs: Bool
|
||||
let component: GifPagerContentComponent
|
||||
public struct ChatMediaInputPaneScrollState {
|
||||
let absoluteOffset: CGFloat?
|
||||
let relativeChange: CGFloat
|
||||
}
|
||||
|
||||
public final class ChatMediaInputGifPaneTrendingState {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
|
||||
init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
}
|
||||
}
|
||||
|
||||
public final class EntityKeyboardGifContent: Equatable {
|
||||
public let hasRecentGifs: Bool
|
||||
public let component: GifPagerContentComponent
|
||||
|
||||
public init(hasRecentGifs: Bool, component: GifPagerContentComponent) {
|
||||
self.hasRecentGifs = hasRecentGifs
|
||||
self.component = component
|
||||
}
|
||||
|
||||
static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
|
||||
public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool {
|
||||
if lhs.hasRecentGifs != rhs.hasRecentGifs {
|
||||
return false
|
||||
}
|
||||
|
|
@ -46,14 +65,14 @@ final class EntityKeyboardGifContent: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
struct InputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
var stickers: EmojiPagerContentComponent?
|
||||
var gifs: EntityKeyboardGifContent?
|
||||
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
public struct InputData: Equatable {
|
||||
public var emoji: EmojiPagerContentComponent
|
||||
public var stickers: EmojiPagerContentComponent?
|
||||
public var gifs: EntityKeyboardGifContent?
|
||||
public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
||||
|
||||
init(
|
||||
public init(
|
||||
emoji: EmojiPagerContentComponent,
|
||||
stickers: EmojiPagerContentComponent?,
|
||||
gifs: EntityKeyboardGifContent?,
|
||||
|
|
@ -66,7 +85,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
}
|
||||
}
|
||||
|
||||
static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
|
||||
public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal<Bool, NoError> {
|
||||
let hasPremium: Signal<Bool, NoError>
|
||||
if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId {
|
||||
hasPremium = .single(true)
|
||||
|
|
@ -83,7 +102,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
return hasPremium
|
||||
}
|
||||
|
||||
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal<InputData, NoError> {
|
||||
public static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction?, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool) -> Signal<InputData, NoError> {
|
||||
let animationCache = context.animationCache
|
||||
let animationRenderer = context.animationRenderer
|
||||
|
||||
|
|
@ -94,7 +113,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId)
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: true, hasTrending: true)
|
||||
|
||||
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|
||||
|> map { appConfiguration -> [String] in
|
||||
|
|
@ -153,7 +172,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchInitiallyHidden: false
|
||||
searchInitiallyHidden: true
|
||||
)
|
||||
))
|
||||
|
||||
|
|
@ -238,11 +257,11 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
}
|
||||
|
||||
var externalTopPanelContainerImpl: PagerExternalTopPanelContainer?
|
||||
override var externalTopPanelContainer: UIView? {
|
||||
public override var externalTopPanelContainer: UIView? {
|
||||
return self.externalTopPanelContainerImpl
|
||||
}
|
||||
|
||||
var switchToTextInput: (() -> Void)?
|
||||
public var switchToTextInput: (() -> Void)?
|
||||
|
||||
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
|
||||
|
||||
|
|
@ -257,7 +276,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
}
|
||||
}
|
||||
|
||||
var canSwitchToTextInputAutomatically: Bool {
|
||||
public var canSwitchToTextInputAutomatically: Bool {
|
||||
if let pagerView = self.entityKeyboardView.componentView as? EntityKeyboardComponent.View, let centralId = pagerView.centralId {
|
||||
if centralId == AnyHashable("emoji") {
|
||||
return false
|
||||
|
|
@ -321,7 +340,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: presentationData.strings.GifSearch_SearchGifPlaceholder,
|
||||
searchInitiallyHidden: false
|
||||
searchInitiallyHidden: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -352,7 +371,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
isLoading: isLoading,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchInitiallyHidden: false
|
||||
searchInitiallyHidden: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -385,7 +404,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchInitiallyHidden: false
|
||||
searchInitiallyHidden: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -466,7 +485,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
isLoading: isLoading,
|
||||
loadMoreToken: loadMoreToken,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchInitiallyHidden: false
|
||||
searchInitiallyHidden: true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -492,8 +511,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
|
||||
private weak var currentUndoOverlayController: UndoOverlayController?
|
||||
|
||||
|
||||
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) {
|
||||
public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, chatPeerId: PeerId?) {
|
||||
self.context = context
|
||||
self.currentInputData = currentInputData
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
|
|
@ -517,7 +535,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] groupId, item, _, _, _, _ in
|
||||
let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in
|
||||
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
|
||||
guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +953,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
guard let controllerInteraction = controllerInteraction else {
|
||||
return
|
||||
}
|
||||
let controller = installedStickerPacksController(context: context, mode: .modal)
|
||||
let controller = context.sharedContext.makeInstalledStickerPacksController(context: context, mode: .modal)
|
||||
controller.navigationPresentation = .modal
|
||||
controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
},
|
||||
|
|
@ -1230,7 +1248,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
}
|
||||
}
|
||||
|
||||
func markInputCollapsed() {
|
||||
public func markInputCollapsed() {
|
||||
self.isMarkInputCollapsed = true
|
||||
}
|
||||
|
||||
|
|
@ -1242,14 +1260,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
|
||||
}
|
||||
|
||||
func simulateUpdateLayout(isVisible: Bool) {
|
||||
public func simulateUpdateLayout(isVisible: Bool) {
|
||||
guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _, isExpanded) = self.currentState else {
|
||||
return
|
||||
}
|
||||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
|
||||
}
|
||||
|
||||
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
|
||||
public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
|
||||
self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded)
|
||||
|
||||
let innerTransition: Transition
|
||||
|
|
@ -1318,11 +1336,14 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
topPanelInsets: UIEdgeInsets(),
|
||||
emojiContent: self.currentInputData.emoji,
|
||||
stickerContent: stickerContent,
|
||||
maskContent: nil,
|
||||
gifContent: gifContent?.component,
|
||||
hasRecentGifs: gifContent?.hasRecentGifs ?? false,
|
||||
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
|
||||
defaultToEmojiTab: self.defaultToEmojiTab,
|
||||
externalTopPanelContainer: self.externalTopPanelContainerImpl,
|
||||
externalBottomPanelContainer: nil,
|
||||
displayTopPanelBackground: false,
|
||||
topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -1502,6 +1523,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||
namespace = Namespaces.ItemCollection.CloudStickerPacks
|
||||
case .emoji:
|
||||
namespace = Namespaces.ItemCollection.CloudEmojiPacks
|
||||
case .masks:
|
||||
namespace = Namespaces.ItemCollection.CloudMaskPacks
|
||||
}
|
||||
|
||||
self.stableReorderableGroupOrder.removeValue(forKey: category)
|
||||
|
|
@ -1685,7 +1708,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent
|
|||
}
|
||||
}
|
||||
|
||||
final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
|
||||
public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, UIInputViewAudioFeedback {
|
||||
private let context: AccountContext
|
||||
|
||||
public var insertText: ((NSAttributedString) -> Void)?
|
||||
|
|
@ -1698,7 +1721,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
|
|||
private let animationCache: AnimationCache
|
||||
private let animationRenderer: MultiAnimationRenderer
|
||||
|
||||
init(
|
||||
public init(
|
||||
context: AccountContext,
|
||||
isDark: Bool,
|
||||
areCustomEmojiEnabled: Bool
|
||||
|
|
@ -1713,8 +1736,8 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
|
|||
self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
|
||||
}
|
||||
|
||||
//super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
|
||||
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
|
||||
super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default)
|
||||
// super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
|
||||
|
||||
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.clipsToBounds = true
|
||||
|
|
@ -1818,7 +1841,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
|
|||
navigationController: {
|
||||
return nil
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _, _ in
|
||||
},
|
||||
|
|
@ -1874,7 +1897,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard let inputNode = self.inputNode else {
|
||||
|
|
@ -2136,3 +2159,112 @@ private final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PaneGifSearchForQueryResult {
|
||||
public let files: [MultiplexedVideoNodeFile]
|
||||
public let nextOffset: String?
|
||||
public let isComplete: Bool
|
||||
public let isStale: Bool
|
||||
|
||||
public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
self.isComplete = isComplete
|
||||
self.isStale = isStale
|
||||
}
|
||||
}
|
||||
|
||||
public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
|
||||
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|
||||
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
|
||||
let botName = searchBots.gifBotUsername ?? "gif"
|
||||
return context.engine.peers.resolvePeerByName(name: botName)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
|
||||
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
|
||||
}
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
} else {
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return maybeDelayedContextResults
|
||||
} else {
|
||||
return .single((nil, true, false))
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
|
||||
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|
||||
let results = collection.results
|
||||
var references: [MultiplexedVideoNodeFile] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
var imageResource: TelegramMediaResource?
|
||||
var thumbnailResource: TelegramMediaResource?
|
||||
var thumbnailIsVideo: Bool = false
|
||||
var uniqueId: Int64?
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
if let resource = content.resource as? WebFileReferenceMediaResource {
|
||||
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
|
||||
}
|
||||
}
|
||||
if let thumbnail = externalReference.thumbnail {
|
||||
thumbnailResource = thumbnail.resource
|
||||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
thumbnailIsVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
var previews: [TelegramMediaImageRepresentation] = []
|
||||
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
|
||||
if let thumbnailResource = thumbnailResource {
|
||||
if thumbnailIsVideo {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
} else {
|
||||
previews.append(TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
))
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let file = internalReference.file {
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
|
||||
} else if incompleteResults {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> beforeStarted {
|
||||
updateActivity?(true)
|
||||
}
|
||||
|> afterCompleted {
|
||||
updateActivity?(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,117 +7,10 @@ import Postbox
|
|||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import WebSearchUI
|
||||
import AppBundle
|
||||
|
||||
class PaneGifSearchForQueryResult {
|
||||
let files: [MultiplexedVideoNodeFile]
|
||||
let nextOffset: String?
|
||||
let isComplete: Bool
|
||||
let isStale: Bool
|
||||
|
||||
init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) {
|
||||
self.files = files
|
||||
self.nextOffset = nextOffset
|
||||
self.isComplete = isComplete
|
||||
self.isStale = isStale
|
||||
}
|
||||
}
|
||||
|
||||
func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal<PaneGifSearchForQueryResult?, NoError> {
|
||||
let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|
||||
|> mapToSignal { searchBots -> Signal<EnginePeer?, NoError> in
|
||||
let botName = searchBots.gifBotUsername ?? "gif"
|
||||
return context.engine.peers.resolvePeerByName(name: botName)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let results = requestContextResults(context: context, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1)
|
||||
|> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in
|
||||
return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false)
|
||||
}
|
||||
|
||||
let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError>
|
||||
if delayRequest {
|
||||
maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue())
|
||||
} else {
|
||||
maybeDelayedContextResults = results
|
||||
}
|
||||
|
||||
return maybeDelayedContextResults
|
||||
} else {
|
||||
return .single((nil, true, false))
|
||||
}
|
||||
}
|
||||
return contextBot
|
||||
|> mapToSignal { result -> Signal<PaneGifSearchForQueryResult?, NoError> in
|
||||
if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection {
|
||||
let results = collection.results
|
||||
var references: [MultiplexedVideoNodeFile] = []
|
||||
for result in results {
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
var imageResource: TelegramMediaResource?
|
||||
var thumbnailResource: TelegramMediaResource?
|
||||
var thumbnailIsVideo: Bool = false
|
||||
var uniqueId: Int64?
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
if let resource = content.resource as? WebFileReferenceMediaResource {
|
||||
uniqueId = Int64(HashFunctions.murMurHash32(resource.url))
|
||||
}
|
||||
}
|
||||
if let thumbnail = externalReference.thumbnail {
|
||||
thumbnailResource = thumbnail.resource
|
||||
if thumbnail.mimeType.hasPrefix("video/") {
|
||||
thumbnailIsVideo = true
|
||||
}
|
||||
}
|
||||
|
||||
if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions {
|
||||
var previews: [TelegramMediaImageRepresentation] = []
|
||||
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
|
||||
if let thumbnailResource = thumbnailResource {
|
||||
if thumbnailIsVideo {
|
||||
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource
|
||||
))
|
||||
} else {
|
||||
previews.append(TelegramMediaImageRepresentation(
|
||||
dimensions: dimensions,
|
||||
resource: thumbnailResource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
))
|
||||
}
|
||||
}
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])])
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let file = internalReference.file {
|
||||
references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2))
|
||||
} else if incompleteResults {
|
||||
return .single(nil)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|> beforeStarted {
|
||||
updateActivity?(true)
|
||||
}
|
||||
|> afterCompleted {
|
||||
updateActivity?(false)
|
||||
}
|
||||
}
|
||||
import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
|
|
@ -6,6 +6,7 @@ import Display
|
|||
import TelegramPresentationData
|
||||
import ActivityIndicator
|
||||
import AppBundle
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)
|
||||
|
|
@ -9,10 +9,13 @@ import TelegramPresentationData
|
|||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import EntityKeyboard
|
||||
import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private let searchBarHeight: CGFloat = 52.0
|
||||
|
||||
protocol PaneSearchContentNode {
|
||||
public protocol PaneSearchContentNode {
|
||||
var ready: Signal<Void, NoError> { get }
|
||||
var deactivateSearchBar: (() -> Void)? { get set }
|
||||
var updateActivity: ((Bool) -> Void)? { get set }
|
||||
|
|
@ -28,7 +31,7 @@ protocol PaneSearchContentNode {
|
|||
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)?
|
||||
}
|
||||
|
||||
final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
||||
public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
||||
private let context: AccountContext
|
||||
private let mode: ChatMediaInputSearchMode
|
||||
public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode
|
||||
|
|
@ -40,15 +43,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
|||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
var onCancel: (() -> Void)?
|
||||
public var onCancel: (() -> Void)?
|
||||
|
||||
var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
|
||||
var ready: Signal<Void, NoError> {
|
||||
public var ready: Signal<Void, NoError> {
|
||||
return self.contentNode.ready
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.controllerInteraction = controllerInteraction
|
||||
|
|
@ -102,7 +105,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
|||
}
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
|
||||
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
|
@ -117,15 +120,15 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
|||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
}
|
||||
|
||||
func updateQuery(_ query: String) {
|
||||
public func updateQuery(_ query: String) {
|
||||
self.searchBar.updateQuery(query)
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
|
|
@ -138,11 +141,11 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
|||
self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition)
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
public func deactivate() {
|
||||
self.searchBar.deactivate(clear: true)
|
||||
}
|
||||
|
||||
func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
|
||||
if let placeholder = placeholder {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
|
|
@ -170,7 +173,7 @@ final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
|||
}
|
||||
}
|
||||
|
||||
func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
if case let .animated(duration, curve) = transition {
|
||||
if let size = self.validLayout {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
|
|
@ -16,20 +16,10 @@ import Emoji
|
|||
import AppBundle
|
||||
import OverlayStatusController
|
||||
import UndoUI
|
||||
|
||||
final class StickerPaneSearchInteraction {
|
||||
let open: (StickerPackCollectionInfo) -> Void
|
||||
let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void
|
||||
let sendSticker: (FileMediaReference, UIView, CGRect) -> Void
|
||||
let getItemIsPreviewed: (StickerPackItem) -> Bool
|
||||
|
||||
init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) {
|
||||
self.open = open
|
||||
self.install = install
|
||||
self.sendSticker = sendSticker
|
||||
self.getItemIsPreviewed = getItemIsPreviewed
|
||||
}
|
||||
}
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import ChatPresentationInterfaceState
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private enum StickerSearchEntryId: Equatable, Hashable {
|
||||
case sticker(String?, Int64)
|
||||
21
submodules/TelegramUI/Components/ChatInputNode/BUILD
Normal file
21
submodules/TelegramUI/Components/ChatInputNode/BUILD
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatInputNode",
|
||||
module_name = "ChatInputNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import ChatPresentationInterfaceState
|
||||
|
||||
open class ChatInputNode: ASDisplayNode {
|
||||
public var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
open var ready: Signal<Void, NoError> {
|
||||
return .single(Void())
|
||||
}
|
||||
|
||||
open var externalTopPanelContainer: UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public var topBackgroundExtension: CGFloat = 0.0
|
||||
public var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
public var hideInput: Bool = false
|
||||
public var adjustLayoutForHiddenInput: Bool = false
|
||||
public var hideInputUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
public var followsDefaultHeight: Bool = false
|
||||
|
||||
open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
|
||||
open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) {
|
||||
return (0.0, 0.0)
|
||||
}
|
||||
}
|
||||
|
|
@ -165,11 +165,14 @@ public final class EmojiStatusSelectionComponent: Component {
|
|||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: component.emojiContent,
|
||||
stickerContent: nil,
|
||||
maskContent: nil,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: true,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
externalBottomPanelContainer: nil,
|
||||
displayTopPanelBackground: true,
|
||||
topPanelExtensionUpdated: { _, _ in },
|
||||
hideInputUpdated: { _, _, _ in },
|
||||
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
|
||||
|
|
|
|||
|
|
@ -482,3 +482,69 @@ public final class EmojiTextAttachmentView: UIView {
|
|||
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
public final class CustomEmojiContainerView: UIView {
|
||||
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
||||
|
||||
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
||||
|
||||
public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func update(fontSize: CGFloat, textColor: UIColor, emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
|
||||
var validKeys = Set<InlineStickerItemLayer.Key>()
|
||||
for (rect, emoji) in emojiRects {
|
||||
let index: Int
|
||||
if let nextIndex = nextIndexById[emoji.fileId] {
|
||||
index = nextIndex
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[emoji.fileId] = index + 1
|
||||
|
||||
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
|
||||
|
||||
let view: UIView
|
||||
if let current = self.emojiLayers[key] {
|
||||
view = current
|
||||
} else if let newView = self.emojiViewProvider(emoji) {
|
||||
view = newView
|
||||
self.addSubview(newView)
|
||||
self.emojiLayers[key] = view
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let view = view as? EmojiTextAttachmentView {
|
||||
view.updateTextColor(textColor)
|
||||
}
|
||||
|
||||
let itemSize: CGFloat = floor(24.0 * fontSize / 17.0)
|
||||
let size = CGSize(width: itemSize, height: itemSize)
|
||||
|
||||
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0) + 1.0), size: size)
|
||||
|
||||
validKeys.insert(key)
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, view) in self.emojiLayers {
|
||||
if !validKeys.contains(key) {
|
||||
removeKeys.append(key)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.emojiLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2101,8 +2101,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||
|
||||
public final class InputInteraction {
|
||||
public let performItemAction: (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void
|
||||
public let deleteBackwards: () -> Void
|
||||
public let openStickerSettings: () -> Void
|
||||
public let deleteBackwards: (() -> Void)?
|
||||
public let openStickerSettings: (() -> Void)?
|
||||
public let openFeatured: () -> Void
|
||||
public let openSearch: () -> Void
|
||||
public let addGroupAction: (AnyHashable, Bool) -> Void
|
||||
|
|
@ -2122,8 +2122,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||
|
||||
public init(
|
||||
performItemAction: @escaping (AnyHashable, Item, UIView, CGRect, CALayer, Bool) -> Void,
|
||||
deleteBackwards: @escaping () -> Void,
|
||||
openStickerSettings: @escaping () -> Void,
|
||||
deleteBackwards: (() -> Void)?,
|
||||
openStickerSettings: (() -> Void)?,
|
||||
openFeatured: @escaping () -> Void,
|
||||
openSearch: @escaping () -> Void,
|
||||
addGroupAction: @escaping (AnyHashable, Bool) -> Void,
|
||||
|
|
@ -6310,7 +6310,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||
selectedItems: Set<MediaId> = Set(),
|
||||
topStatusTitle: String? = nil,
|
||||
topicTitle: String? = nil,
|
||||
topicColor: Int32? = nil
|
||||
topicColor: Int32? = nil,
|
||||
hasSearch: Bool = true
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
|
@ -7088,15 +7089,17 @@ public final class EmojiPagerContentComponent: Component {
|
|||
|
||||
var displaySearchWithPlaceholder: String?
|
||||
var searchInitiallyHidden = true
|
||||
if isReactionSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder
|
||||
} else if isStatusSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder
|
||||
} else if isTopicIconSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchTopicIconsPlaceholder
|
||||
} else if isEmojiSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
|
||||
searchInitiallyHidden = false
|
||||
if hasSearch {
|
||||
if isReactionSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder
|
||||
} else if isStatusSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder
|
||||
} else if isTopicIconSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchTopicIconsPlaceholder
|
||||
} else if isEmojiSelection {
|
||||
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
|
||||
searchInitiallyHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent(
|
||||
|
|
@ -7160,7 +7163,9 @@ public final class EmojiPagerContentComponent: Component {
|
|||
animationRenderer: MultiAnimationRenderer,
|
||||
stickerNamespaces: [ItemCollectionId.Namespace],
|
||||
stickerOrderedItemListCollectionIds: [Int32],
|
||||
chatPeerId: EnginePeer.Id?
|
||||
chatPeerId: EnginePeer.Id?,
|
||||
hasSearch: Bool,
|
||||
hasTrending: Bool
|
||||
) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||
|
|
@ -7212,7 +7217,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000),
|
||||
hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false),
|
||||
context.account.viewTracker.featuredStickerPacks(),
|
||||
hasTrending ? context.account.viewTracker.featuredStickerPacks() : .single([]),
|
||||
context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))),
|
||||
ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager),
|
||||
peerSpecificPack
|
||||
|
|
@ -7674,8 +7679,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||
itemLayoutType: .detailed,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: false,
|
||||
displaySearchWithPlaceholder: strings.StickersSearch_SearchStickersPlaceholder,
|
||||
searchInitiallyHidden: false,
|
||||
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
|
||||
searchInitiallyHidden: true,
|
||||
searchIsPlaceholderOnly: true,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: false,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public final class EntityKeyboardComponent: Component {
|
|||
public enum ReorderCategory {
|
||||
case stickers
|
||||
case emoji
|
||||
case masks
|
||||
}
|
||||
|
||||
public struct GifSearchEmoji: Equatable {
|
||||
|
|
@ -92,11 +93,14 @@ public final class EntityKeyboardComponent: Component {
|
|||
public let topPanelInsets: UIEdgeInsets
|
||||
public let emojiContent: EmojiPagerContentComponent?
|
||||
public let stickerContent: EmojiPagerContentComponent?
|
||||
public let maskContent: EmojiPagerContentComponent?
|
||||
public let gifContent: GifPagerContentComponent?
|
||||
public let hasRecentGifs: Bool
|
||||
public let availableGifSearchEmojies: [GifSearchEmoji]
|
||||
public let defaultToEmojiTab: Bool
|
||||
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let externalBottomPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let displayTopPanelBackground: Bool
|
||||
public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void
|
||||
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
|
||||
public let hideTopPanelUpdated: (Bool, Transition) -> Void
|
||||
|
|
@ -118,11 +122,14 @@ public final class EntityKeyboardComponent: Component {
|
|||
topPanelInsets: UIEdgeInsets,
|
||||
emojiContent: EmojiPagerContentComponent?,
|
||||
stickerContent: EmojiPagerContentComponent?,
|
||||
maskContent: EmojiPagerContentComponent?,
|
||||
gifContent: GifPagerContentComponent?,
|
||||
hasRecentGifs: Bool,
|
||||
availableGifSearchEmojies: [GifSearchEmoji],
|
||||
defaultToEmojiTab: Bool,
|
||||
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
||||
externalBottomPanelContainer: PagerExternalTopPanelContainer?,
|
||||
displayTopPanelBackground: Bool,
|
||||
topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void,
|
||||
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
||||
hideTopPanelUpdated: @escaping (Bool, Transition) -> Void,
|
||||
|
|
@ -143,11 +150,14 @@ public final class EntityKeyboardComponent: Component {
|
|||
self.topPanelInsets = topPanelInsets
|
||||
self.emojiContent = emojiContent
|
||||
self.stickerContent = stickerContent
|
||||
self.maskContent = maskContent
|
||||
self.gifContent = gifContent
|
||||
self.hasRecentGifs = hasRecentGifs
|
||||
self.availableGifSearchEmojies = availableGifSearchEmojies
|
||||
self.defaultToEmojiTab = defaultToEmojiTab
|
||||
self.externalTopPanelContainer = externalTopPanelContainer
|
||||
self.externalBottomPanelContainer = externalBottomPanelContainer
|
||||
self.displayTopPanelBackground = displayTopPanelBackground
|
||||
self.topPanelExtensionUpdated = topPanelExtensionUpdated
|
||||
self.hideInputUpdated = hideInputUpdated
|
||||
self.hideTopPanelUpdated = hideTopPanelUpdated
|
||||
|
|
@ -184,6 +194,9 @@ public final class EntityKeyboardComponent: Component {
|
|||
if lhs.stickerContent != rhs.stickerContent {
|
||||
return false
|
||||
}
|
||||
if lhs.maskContent != rhs.maskContent {
|
||||
return false
|
||||
}
|
||||
if lhs.gifContent != rhs.gifContent {
|
||||
return false
|
||||
}
|
||||
|
|
@ -199,6 +212,9 @@ public final class EntityKeyboardComponent: Component {
|
|||
if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer {
|
||||
return false
|
||||
}
|
||||
if lhs.displayTopPanelBackground != rhs.displayTopPanelBackground {
|
||||
return false
|
||||
}
|
||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
|
|
@ -268,11 +284,96 @@ public final class EntityKeyboardComponent: Component {
|
|||
|
||||
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
|
||||
let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
|
||||
let masksContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
|
||||
|
||||
if transition.userData(MarkInputCollapsed.self) != nil {
|
||||
self.searchComponent = nil
|
||||
}
|
||||
|
||||
if let maskContent = component.maskContent {
|
||||
var topMaskItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
|
||||
for itemGroup in maskContent.itemGroups {
|
||||
if let id = itemGroup.supergroupId.base as? String {
|
||||
let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [
|
||||
"saved": .saved,
|
||||
"recent": .recent,
|
||||
"premium": .premium
|
||||
]
|
||||
let titleMapping: [String: String] = [
|
||||
"saved": component.strings.Stickers_Favorites,
|
||||
"recent": component.strings.Stickers_Recent,
|
||||
"premium": component.strings.EmojiInput_PanelTitlePremium
|
||||
]
|
||||
if let icon = iconMapping[id], let title = titleMapping[id] {
|
||||
topMaskItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: itemGroup.supergroupId,
|
||||
isReorderable: false,
|
||||
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||
icon: icon,
|
||||
theme: component.theme,
|
||||
useAccentColor: false,
|
||||
title: title,
|
||||
pressed: { [weak self] in
|
||||
self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil)
|
||||
}
|
||||
))
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if !itemGroup.items.isEmpty {
|
||||
if let animationData = itemGroup.items[0].animationData {
|
||||
topMaskItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: itemGroup.supergroupId,
|
||||
isReorderable: !itemGroup.isFeatured,
|
||||
content: AnyComponent(EntityKeyboardAnimationTopPanelComponent(
|
||||
context: maskContent.context,
|
||||
item: itemGroup.headerItem ?? animationData,
|
||||
isFeatured: itemGroup.isFeatured,
|
||||
isPremiumLocked: itemGroup.isPremiumLocked,
|
||||
animationCache: maskContent.animationCache,
|
||||
animationRenderer: maskContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: itemGroup.title ?? "",
|
||||
pressed: { [weak self] in
|
||||
self?.scrollToItemGroup(contentId: "masks", groupId: itemGroup.supergroupId, subgroupId: nil)
|
||||
}
|
||||
))
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
contents.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(maskContent)))
|
||||
contentTopPanels.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(EntityKeyboardTopPanelComponent(
|
||||
id: "masks",
|
||||
theme: component.theme,
|
||||
items: topMaskItems,
|
||||
containerSideInset: component.containerInsets.left + component.topPanelInsets.left,
|
||||
defaultActiveItemId: maskContent.itemGroups.first?.groupId,
|
||||
activeContentItemIdUpdated: masksContentItemIdUpdated,
|
||||
reorderItems: { [weak self] items in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.reorderPacks(category: .stickers, items: items)
|
||||
}
|
||||
))))
|
||||
contentIcons.append(PagerComponentContentIcon(id: "masks", imageName: "Chat/Input/Media/EntityInputMasksIcon"))
|
||||
if let _ = component.maskContent?.inputInteractionHolder.inputInteraction?.openStickerSettings {
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "masks", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputSettingsIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: {
|
||||
maskContent.inputInteractionHolder.inputInteraction?.openStickerSettings?()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
}
|
||||
|
||||
if let gifContent = component.gifContent {
|
||||
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(gifContent)))
|
||||
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
|
|
@ -345,16 +446,6 @@ public final class EntityKeyboardComponent: Component {
|
|||
}
|
||||
))))
|
||||
contentIcons.append(PagerComponentContentIcon(id: "gifs", imageName: "Chat/Input/Media/EntityInputGifsIcon"))
|
||||
// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button(
|
||||
// content: AnyComponent(BundleIconComponent(
|
||||
// name: "Chat/Input/Media/EntityInputSearchIcon",
|
||||
// tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
// maxSize: nil
|
||||
// )),
|
||||
// action: { [weak self] in
|
||||
// self?.openSearch()
|
||||
// }
|
||||
// ).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
|
||||
if let stickerContent = component.stickerContent {
|
||||
|
|
@ -459,28 +550,22 @@ public final class EntityKeyboardComponent: Component {
|
|||
}
|
||||
))))
|
||||
contentIcons.append(PagerComponentContentIcon(id: "stickers", imageName: "Chat/Input/Media/EntityInputStickersIcon"))
|
||||
// contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
|
||||
// content: AnyComponent(BundleIconComponent(
|
||||
// name: "Chat/Input/Media/EntityInputSearchIcon",
|
||||
// tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
// maxSize: nil
|
||||
// )),
|
||||
// action: { [weak self] in
|
||||
// self?.openSearch()
|
||||
// }
|
||||
// ).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputSettingsIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: {
|
||||
stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
if let _ = component.stickerContent?.inputInteractionHolder.inputInteraction?.openStickerSettings {
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputSettingsIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: {
|
||||
stickerContent.inputInteractionHolder.inputInteraction?.openStickerSettings?()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
}
|
||||
|
||||
let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards
|
||||
|
||||
let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>()
|
||||
if let emojiContent = component.emojiContent {
|
||||
contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(emojiContent)))
|
||||
|
|
@ -564,36 +649,39 @@ public final class EntityKeyboardComponent: Component {
|
|||
}
|
||||
))))
|
||||
contentIcons.append(PagerComponentContentIcon(id: "emoji", imageName: "Chat/Input/Media/EntityInputEmojiIcon"))
|
||||
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
if let _ = deleteBackwards {
|
||||
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputGlobeIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
component.switchToTextInput()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = deleteBackwards {
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputGlobeIcon",
|
||||
name: "Chat/Input/Media/EntityInputClearIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
component.switchToTextInput()
|
||||
action: {
|
||||
deleteBackwards?()
|
||||
AudioServicesPlaySystemSound(1155)
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
|
||||
let deleteBackwards = component.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputClearIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: {
|
||||
).withHoldAction({
|
||||
deleteBackwards?()
|
||||
AudioServicesPlaySystemSound(1155)
|
||||
}
|
||||
).withHoldAction({
|
||||
deleteBackwards?()
|
||||
AudioServicesPlaySystemSound(1155)
|
||||
}).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
}
|
||||
|
||||
let panelHideBehavior: PagerComponentPanelHideBehavior
|
||||
if self.searchComponent != nil {
|
||||
|
|
@ -618,17 +706,18 @@ public final class EntityKeyboardComponent: Component {
|
|||
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
|
||||
theme: component.theme,
|
||||
overflowHeight: component.hiddenInputHeight,
|
||||
displayBackground: component.externalTopPanelContainer == nil
|
||||
displayBackground: component.externalTopPanelContainer == nil && component.displayTopPanelBackground
|
||||
)),
|
||||
externalTopPanelContainer: component.externalTopPanelContainer,
|
||||
bottomPanel: component.displayBottomPanel ? AnyComponent(EntityKeyboardBottomPanelComponent(
|
||||
theme: component.theme,
|
||||
containerInsets: component.containerInsets,
|
||||
deleteBackwards: { [weak self] in
|
||||
self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards()
|
||||
self?.component?.emojiContent?.inputInteractionHolder.inputInteraction?.deleteBackwards?()
|
||||
AudioServicesPlaySystemSound(0x451)
|
||||
}
|
||||
)) : nil,
|
||||
externalBottomPanelContainer: component.externalBottomPanelContainer,
|
||||
panelStateUpdated: { [weak self] panelState, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -661,6 +750,8 @@ public final class EntityKeyboardComponent: Component {
|
|||
return stickersContentItemIdUpdated
|
||||
} else if id == AnyHashable("emoji") {
|
||||
return emojiContentItemIdUpdated
|
||||
} else if id == AnyHashable("masks") {
|
||||
return masksContentItemIdUpdated
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -670,7 +761,7 @@ public final class EntityKeyboardComponent: Component {
|
|||
)
|
||||
transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize))
|
||||
|
||||
let accountContext = component.emojiContent?.context ?? component.stickerContent?.context
|
||||
let accountContext = component.emojiContent?.context ?? component.maskContent?.context
|
||||
if let searchComponent = self.searchComponent, let accountContext = accountContext {
|
||||
var animateIn = false
|
||||
let searchView: ComponentHostView<EntitySearchContentEnvironment>
|
||||
|
|
|
|||
|
|
@ -401,11 +401,14 @@ private final class TopicIconSelectionComponent: Component {
|
|||
topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0),
|
||||
emojiContent: component.emojiContent,
|
||||
stickerContent: nil,
|
||||
maskContent: nil,
|
||||
gifContent: nil,
|
||||
hasRecentGifs: false,
|
||||
availableGifSearchEmojies: [],
|
||||
defaultToEmojiTab: true,
|
||||
externalTopPanelContainer: self.panelHostView,
|
||||
externalBottomPanelContainer: nil,
|
||||
displayTopPanelBackground: true,
|
||||
topPanelExtensionUpdated: { _, _ in },
|
||||
hideInputUpdated: { _, _, _ in },
|
||||
hideTopPanelUpdated: { _, _ in },
|
||||
|
|
|
|||
26
submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD
Normal file
26
submodules/TelegramUI/Components/MultiplexedVideoNode/BUILD
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "MultiplexedVideoNode",
|
||||
module_name = "MultiplexedVideoNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -74,24 +74,24 @@ private final class VisibleVideoItem {
|
|||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNodeFile {
|
||||
let file: FileMediaReference
|
||||
let contextResult: (ChatContextResultCollection, ChatContextResult)?
|
||||
public final class MultiplexedVideoNodeFile {
|
||||
public let file: FileMediaReference
|
||||
public let contextResult: (ChatContextResultCollection, ChatContextResult)?
|
||||
|
||||
init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
|
||||
public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
|
||||
self.file = file
|
||||
self.contextResult = contextResult
|
||||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNodeFiles {
|
||||
let saved: [MultiplexedVideoNodeFile]
|
||||
let trending: [MultiplexedVideoNodeFile]
|
||||
let isSearch: Bool
|
||||
let canLoadMore: Bool
|
||||
let isStale: Bool
|
||||
public final class MultiplexedVideoNodeFiles {
|
||||
public let saved: [MultiplexedVideoNodeFile]
|
||||
public let trending: [MultiplexedVideoNodeFile]
|
||||
public let isSearch: Bool
|
||||
public let canLoadMore: Bool
|
||||
public let isStale: Bool
|
||||
|
||||
init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) {
|
||||
public init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) {
|
||||
self.saved = saved
|
||||
self.trending = trending
|
||||
self.isSearch = isSearch
|
||||
|
|
@ -100,36 +100,37 @@ final class MultiplexedVideoNodeFiles {
|
|||
}
|
||||
}
|
||||
|
||||
final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let account: Account
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
private let trackingNode: MultiplexedVideoTrackingNode
|
||||
var didScroll: ((CGFloat, CGFloat) -> Void)?
|
||||
var didEndScrolling: (() -> Void)?
|
||||
var reactionSelected: ((String) -> Void)?
|
||||
|
||||
var topInset: CGFloat = 0.0 {
|
||||
public var didScroll: ((CGFloat, CGFloat) -> Void)?
|
||||
public var didEndScrolling: (() -> Void)?
|
||||
public var reactionSelected: ((String) -> Void)?
|
||||
|
||||
public var topInset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var bottomInset: CGFloat = 0.0 {
|
||||
public var bottomInset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var idealHeight: CGFloat = 93.0 {
|
||||
public var idealHeight: CGFloat = 93.0 {
|
||||
didSet {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false)
|
||||
public private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false)
|
||||
|
||||
func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
|
||||
public func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
|
||||
self.files = files
|
||||
|
||||
self.ignoreDidScroll = true
|
||||
|
|
@ -145,7 +146,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:]
|
||||
|
||||
private let contextContainerNode: ContextControllerSourceNode
|
||||
let scrollNode: ASScrollNode
|
||||
public let scrollNode: ASScrollNode
|
||||
|
||||
private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
|
||||
|
||||
|
|
@ -157,14 +158,14 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
|
||||
private let timebase: CMTimebase
|
||||
|
||||
var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
|
||||
var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
var enableVideoNodes = false
|
||||
public var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
|
||||
public var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
public var enableVideoNodes = false
|
||||
|
||||
private var currentActivatingId: VisibleVideoItem.Id?
|
||||
private var isFinishingActivation = false
|
||||
|
||||
init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
public init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.account = account
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
|
@ -343,7 +344,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
}
|
||||
|
||||
private var validSize: CGSize?
|
||||
func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
public func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
if self.validSize == nil || !self.validSize!.equalTo(size) {
|
||||
|
|
@ -359,18 +360,18 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
|
||||
private var ignoreDidScroll: Bool = false
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreDidScroll {
|
||||
self.updateImmediatelyVisibleItems()
|
||||
self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
self.didEndScrolling?()
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
self.didEndScrolling?()
|
||||
}
|
||||
|
|
@ -738,7 +739,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func frameForItem(_ id: MediaId) -> CGRect? {
|
||||
public func frameForItem(_ id: MediaId) -> CGRect? {
|
||||
for item in self.displayItems {
|
||||
if item.file.file.media.fileId == id {
|
||||
return item.frame
|
||||
|
|
@ -747,7 +748,7 @@ final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
return nil
|
||||
}
|
||||
|
||||
func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
public func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
|
||||
if let result = self.internalFileAt(point: point) {
|
||||
return (result.1, result.2, result.3)
|
||||
} else {
|
||||
|
|
@ -13,7 +13,7 @@ private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction {
|
|||
}
|
||||
}
|
||||
|
||||
final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
||||
public final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
||||
private let usePlaceholder: Bool
|
||||
private var placeholder: MultiplexedVideoPlaceholderNode?
|
||||
private var theme: PresentationTheme?
|
||||
|
|
@ -21,7 +21,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
|||
|
||||
var disposable = MetaDisposable()
|
||||
|
||||
var ready: (() -> Void)? {
|
||||
public var ready: (() -> Void)? {
|
||||
didSet {
|
||||
if self.layer.contents != nil {
|
||||
self.ready?()
|
||||
|
|
@ -29,6 +29,10 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
public convenience init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false) {
|
||||
self.init(account: account, fileReference: fileReference, synchronousLoad: synchronousLoad, existingPlaceholder: nil)
|
||||
}
|
||||
|
||||
init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool, usePlaceholder: Bool = false, existingPlaceholder: MultiplexedVideoPlaceholderNode? = nil) {
|
||||
self.usePlaceholder = usePlaceholder
|
||||
if usePlaceholder {
|
||||
|
|
@ -102,7 +106,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
|||
self.disposable.dispose()
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, size: CGSize) {
|
||||
public func update(theme: PresentationTheme, size: CGSize) {
|
||||
if self.usePlaceholder {
|
||||
self.theme = theme
|
||||
}
|
||||
|
|
@ -112,7 +116,7 @@ final class SoftwareVideoThumbnailNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
|
||||
public func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
|
||||
self.asolutePosition = (absoluteRect, containerSize)
|
||||
if let placeholder = self.placeholder {
|
||||
placeholder.updateAbsoluteRect(absoluteRect, within: containerSize)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "mask.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/mask.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputMasksIcon.imageset/mask.pdf
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue