mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Various improvements
This commit is contained in:
parent
2028561610
commit
3b11fa400e
@ -55,7 +55,7 @@ final class ChatListContainerItemNode: ASDisplayNode {
|
||||
private(set) var validLayout: (size: CGSize, insets: UIEdgeInsets, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
|
||||
private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
|
||||
|
||||
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool) {
|
||||
init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, filter: ChatListFilter?, chatListMode: ChatListNodeMode, previewing: Bool, isInlineMode: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, becameEmpty: @escaping (ChatListFilter?) -> Void, emptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void, autoSetReady: Bool, isMainTab: Bool?) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.location = location
|
||||
@ -68,7 +68,7 @@ final class ChatListContainerItemNode: ASDisplayNode {
|
||||
self.openArchiveSettings = openArchiveSettings
|
||||
self.isInlineMode = isInlineMode
|
||||
|
||||
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady)
|
||||
self.listNode = ChatListNode(context: context, location: location, chatListFilter: filter, previewing: previewing, fillPreloadItems: controlsHistoryPreload, mode: chatListMode, theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: animationCache, animationRenderer: animationRenderer, disableAnimations: true, isInlineMode: isInlineMode, autoSetReady: autoSetReady, isMainTab: isMainTab)
|
||||
|
||||
if let controller, case .chatList(groupId: .root) = controller.location {
|
||||
self.listNode.scrollHeightTopInset = ChatListNavigationBar.searchScrollHeight + ChatListNavigationBar.storiesScrollHeight
|
||||
|
@ -440,7 +440,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
self?.secondaryEmptyAction()
|
||||
}, openArchiveSettings: { [weak self] in
|
||||
self?.openArchiveSettings()
|
||||
}, autoSetReady: true)
|
||||
}, autoSetReady: true, isMainTab: nil)
|
||||
self.itemNodes[.all] = itemNode
|
||||
self.addSubnode(itemNode)
|
||||
|
||||
@ -784,7 +784,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
self?.secondaryEmptyAction()
|
||||
}, openArchiveSettings: { [weak self] in
|
||||
self?.openArchiveSettings()
|
||||
}, autoSetReady: !animated)
|
||||
}, autoSetReady: !animated, isMainTab: index == 0)
|
||||
let disposable = MetaDisposable()
|
||||
self.pendingItemNode = (id, itemNode, disposable)
|
||||
|
||||
@ -930,7 +930,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
self?.secondaryEmptyAction()
|
||||
}, openArchiveSettings: { [weak self] in
|
||||
self?.openArchiveSettings()
|
||||
}, autoSetReady: false)
|
||||
}, autoSetReady: false, isMainTab: i == 0)
|
||||
itemNode.listNode.tempTopInset = self.tempTopInset
|
||||
self.itemNodes[id] = itemNode
|
||||
}
|
||||
@ -969,6 +969,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
}
|
||||
}
|
||||
|
||||
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0 ? true : false)
|
||||
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
|
||||
if let scrollingOffset = self.scrollingOffset {
|
||||
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition)
|
||||
|
@ -2358,6 +2358,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
break
|
||||
}
|
||||
}
|
||||
storyStatsIds.removeAll(where: { $0 == context.account.peerId })
|
||||
|
||||
return context.engine.data.subscribe(
|
||||
EngineDataMap(
|
||||
@ -2551,6 +2552,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
|> map { stats -> ([RecentlySearchedPeer], [EnginePeer.Id: PeerStoryStats]) in
|
||||
var mappedStats: [EnginePeer.Id: PeerStoryStats] = [:]
|
||||
for (id, value) in stats {
|
||||
if id == context.account.peerId {
|
||||
continue
|
||||
}
|
||||
if let value {
|
||||
mappedStats[id] = value
|
||||
}
|
||||
|
@ -718,7 +718,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
}
|
||||
case let .buttonChoice(isPositive):
|
||||
switch notice {
|
||||
case let .reviewLogin(newSessionReview):
|
||||
case let .reviewLogin(newSessionReview, _):
|
||||
nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive)
|
||||
default:
|
||||
break
|
||||
@ -1038,7 +1038,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
}
|
||||
case let .buttonChoice(isPositive):
|
||||
switch notice {
|
||||
case let .reviewLogin(newSessionReview):
|
||||
case let .reviewLogin(newSessionReview, _):
|
||||
nodeInteraction?.performActiveSessionAction(newSessionReview, isPositive)
|
||||
default:
|
||||
break
|
||||
@ -1250,6 +1250,7 @@ public final class ChatListNode: ListView {
|
||||
private let chatFolderUpdates = Promise<ChatFolderUpdates?>()
|
||||
private var pollFilterUpdatesDisposable: Disposable?
|
||||
private var chatFilterUpdatesDisposable: Disposable?
|
||||
private var updateIsMainTabDisposable: Disposable?
|
||||
|
||||
public var scrollHeightTopInset: CGFloat {
|
||||
didSet {
|
||||
@ -1261,7 +1262,10 @@ public final class ChatListNode: ListView {
|
||||
|
||||
private let autoSetReady: Bool
|
||||
|
||||
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool) {
|
||||
public let isMainTab = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let suggestedChatListNotice = Promise<ChatListNotice?>(nil)
|
||||
|
||||
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool, autoSetReady: Bool, isMainTab: Bool?) {
|
||||
self.context = context
|
||||
self.location = location
|
||||
self.chatListFilter = chatListFilter
|
||||
@ -1272,7 +1276,9 @@ public final class ChatListNode: ListView {
|
||||
self.animationRenderer = animationRenderer
|
||||
self.autoSetReady = autoSetReady
|
||||
|
||||
let isMainTab = chatListFilter == nil && location == .chatList(groupId: .root)
|
||||
if let isMainTab {
|
||||
self.isMainTab.set(isMainTab)
|
||||
}
|
||||
|
||||
var isSelecting = false
|
||||
if case .peers(_, true, _, _, _, _) = mode {
|
||||
@ -1749,76 +1755,84 @@ public final class ChatListNode: ListView {
|
||||
displayArchiveIntro = .single(false)
|
||||
}
|
||||
|
||||
let suggestedChatListNotice: Signal<ChatListNotice?, NoError>
|
||||
if case .chatList(groupId: .root) = location, chatListFilter == nil {
|
||||
self.updateIsMainTabDisposable = (self.isMainTab.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isMainTab in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard case .chatList(groupId: .root) = location, isMainTab else {
|
||||
self.suggestedChatListNotice.set(.single(nil))
|
||||
return
|
||||
}
|
||||
|
||||
let _ = context.engine.privacy.cleanupSessionReviews().start()
|
||||
|
||||
suggestedChatListNotice = .single(nil)
|
||||
|> then (
|
||||
combineLatest(
|
||||
getServerProvidedSuggestions(account: context.account),
|
||||
context.engine.auth.twoStepVerificationConfiguration(),
|
||||
newSessionReviews(postbox: context.account.postbox)
|
||||
)
|
||||
|> mapToSignal { suggestions, configuration, newSessionReviews -> Signal<ChatListNotice?, NoError> in
|
||||
if let newSessionReview = newSessionReviews.first {
|
||||
return .single(.reviewLogin(newSessionReview: newSessionReview))
|
||||
let twoStepData: Signal<TwoStepVerificationConfiguration?, NoError> = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init))
|
||||
|
||||
let suggestedChatListNoticeSignal: Signal<ChatListNotice?, NoError> = combineLatest(
|
||||
getServerProvidedSuggestions(account: context.account),
|
||||
twoStepData,
|
||||
newSessionReviews(postbox: context.account.postbox)
|
||||
)
|
||||
|> mapToSignal { suggestions, configuration, newSessionReviews -> Signal<ChatListNotice?, NoError> in
|
||||
if let newSessionReview = newSessionReviews.first {
|
||||
return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count))
|
||||
}
|
||||
if suggestions.contains(.setupPassword), let configuration {
|
||||
var notSet = false
|
||||
switch configuration {
|
||||
case let .notSet(pendingEmail):
|
||||
if pendingEmail == nil {
|
||||
notSet = true
|
||||
}
|
||||
case .set:
|
||||
break
|
||||
}
|
||||
if suggestions.contains(.setupPassword) {
|
||||
var notSet = false
|
||||
switch configuration {
|
||||
case let .notSet(pendingEmail):
|
||||
if pendingEmail == nil {
|
||||
notSet = true
|
||||
}
|
||||
case .set:
|
||||
break
|
||||
}
|
||||
if notSet {
|
||||
return .single(.setupPassword)
|
||||
}
|
||||
}
|
||||
if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
return inAppPurchaseManager.availableProducts
|
||||
|> map { products -> ChatListNotice? in
|
||||
if products.count > 1 {
|
||||
let shortestOptionPrice: (Int64, NSDecimalNumber)
|
||||
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
|
||||
} else {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
for product in products {
|
||||
if product.id.hasSuffix(".annual") {
|
||||
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0)
|
||||
let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
if discount > 0 {
|
||||
if suggestions.contains(.restorePremium) {
|
||||
return .premiumRestore(discount: discount)
|
||||
} else if suggestions.contains(.annualPremium) {
|
||||
return .premiumAnnualDiscount(discount: discount)
|
||||
} else if suggestions.contains(.upgradePremium) {
|
||||
return .premiumUpgrade(discount: discount)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
if notSet {
|
||||
return .single(.setupPassword)
|
||||
}
|
||||
}
|
||||
)
|
||||
if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
return inAppPurchaseManager.availableProducts
|
||||
|> map { products -> ChatListNotice? in
|
||||
if products.count > 1 {
|
||||
let shortestOptionPrice: (Int64, NSDecimalNumber)
|
||||
if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) {
|
||||
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
|
||||
} else {
|
||||
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
|
||||
}
|
||||
for product in products {
|
||||
if product.id.hasSuffix(".annual") {
|
||||
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(12) / Float(shortestOptionPrice.0)
|
||||
let discount = Int32(round((1.0 - fraction) * 20.0) * 5.0)
|
||||
if discount > 0 {
|
||||
if suggestions.contains(.restorePremium) {
|
||||
return .premiumRestore(discount: discount)
|
||||
} else if suggestions.contains(.annualPremium) {
|
||||
return .premiumAnnualDiscount(discount: discount)
|
||||
} else if suggestions.contains(.upgradePremium) {
|
||||
return .premiumUpgrade(discount: discount)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
} else {
|
||||
suggestedChatListNotice = .single(nil)
|
||||
}
|
||||
|
||||
self.suggestedChatListNotice.set(suggestedChatListNoticeSignal)
|
||||
})
|
||||
|
||||
let storageInfo: Signal<Double?, NoError>
|
||||
if !"".isEmpty, case .chatList(groupId: .root) = location, chatListFilter == nil {
|
||||
@ -2008,7 +2022,7 @@ public final class ChatListNode: ListView {
|
||||
hideArchivedFolderByDefault,
|
||||
displayArchiveIntro,
|
||||
storageInfo,
|
||||
suggestedChatListNotice,
|
||||
suggestedChatListNotice.get(),
|
||||
savedMessagesPeer,
|
||||
chatListViewUpdate,
|
||||
self.statePromise.get(),
|
||||
@ -2028,7 +2042,9 @@ public final class ChatListNode: ListView {
|
||||
notice = nil
|
||||
}
|
||||
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: isMainTab)
|
||||
let innerIsMainTab = location == .chatList(groupId: .root) && chatListFilter == nil
|
||||
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(view: update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts, accountPeerId: accountPeerId, isMainTab: innerIsMainTab)
|
||||
var isEmpty = true
|
||||
var entries = rawEntries.filter { entry in
|
||||
switch entry {
|
||||
@ -2983,6 +2999,7 @@ public final class ChatListNode: ListView {
|
||||
self.updatedFilterDisposable.dispose()
|
||||
self.pollFilterUpdatesDisposable?.dispose()
|
||||
self.chatFilterUpdatesDisposable?.dispose()
|
||||
self.updateIsMainTabDisposable?.dispose()
|
||||
}
|
||||
|
||||
func updateFilter(_ filter: ChatListFilter?) {
|
||||
|
@ -85,7 +85,7 @@ enum ChatListNotice: Equatable {
|
||||
case premiumUpgrade(discount: Int32)
|
||||
case premiumAnnualDiscount(discount: Int32)
|
||||
case premiumRestore(discount: Int32)
|
||||
case reviewLogin(newSessionReview: NewSessionReview)
|
||||
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
|
||||
}
|
||||
|
||||
enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
|
@ -203,11 +203,15 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .reviewLogin(newSessionReview):
|
||||
case let .reviewLogin(newSessionReview, totalCount):
|
||||
spacing = 2.0
|
||||
alignment = .center
|
||||
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: item.strings.ChatList_SessionReview_PanelTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
var rawTitleString = item.strings.ChatList_SessionReview_PanelTitle
|
||||
if totalCount > 1 {
|
||||
rawTitleString = "1/\(totalCount) \(rawTitleString)"
|
||||
}
|
||||
let titleStringValue = NSMutableAttributedString(attributedString: NSAttributedString(string: rawTitleString, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor))
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_SessionReview_PanelText(newSessionReview.device, newSessionReview.location).string, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
|
@ -18,6 +18,34 @@ import FastBlur
|
||||
import MediaEditor
|
||||
import RadialStatusNode
|
||||
|
||||
private let leftShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: -1.0, y: 1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
private let rightShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
enum MediaPickerGridItemContent: Equatable {
|
||||
case asset(PHFetchResult<PHAsset>, Int)
|
||||
case media(MediaPickerScreen.Subject.Media, Int)
|
||||
@ -78,19 +106,6 @@ final class MediaPickerGridItem: GridItem {
|
||||
}
|
||||
}
|
||||
|
||||
private let maskImage = generateImage(CGSize(width: 1.0, height: 36.0), opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.45).cgColor] as CFArray
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
|
||||
final class MediaPickerGridItemNode: GridItemNode {
|
||||
var currentMediaState: (TGMediaSelectableItem, Int)?
|
||||
var currentAssetState: (PHFetchResult<PHAsset>, Int)?
|
||||
@ -104,7 +119,8 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let imageNode: ImageNode
|
||||
private var checkNode: InteractiveCheckNode?
|
||||
private let gradientNode: ASImageNode
|
||||
private let leftShadowNode: ASImageNode
|
||||
private let rightShadowNode: ASImageNode
|
||||
private let typeIconNode: ASImageNode
|
||||
private let durationNode: ImmediateTextNode
|
||||
private let draftNode: ImmediateTextNode
|
||||
@ -135,11 +151,17 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.animateFirstTransition = false
|
||||
|
||||
self.gradientNode = ASImageNode()
|
||||
self.gradientNode.displaysAsynchronously = false
|
||||
self.gradientNode.displayWithoutProcessing = true
|
||||
self.gradientNode.image = maskImage
|
||||
self.gradientNode.isLayerBacked = true
|
||||
self.leftShadowNode = ASImageNode()
|
||||
self.leftShadowNode.displaysAsynchronously = false
|
||||
self.leftShadowNode.displayWithoutProcessing = true
|
||||
self.leftShadowNode.image = leftShadowImage
|
||||
self.leftShadowNode.isLayerBacked = true
|
||||
|
||||
self.rightShadowNode = ASImageNode()
|
||||
self.rightShadowNode.displaysAsynchronously = false
|
||||
self.rightShadowNode.displayWithoutProcessing = true
|
||||
self.rightShadowNode.image = rightShadowImage
|
||||
self.rightShadowNode.isLayerBacked = true
|
||||
|
||||
self.typeIconNode = ASImageNode()
|
||||
self.typeIconNode.displaysAsynchronously = false
|
||||
@ -148,6 +170,8 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
|
||||
self.durationNode = ImmediateTextNode()
|
||||
self.durationNode.isLayerBacked = true
|
||||
self.durationNode.textShadowColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
self.durationNode.textShadowBlur = 4.0
|
||||
self.draftNode = ImmediateTextNode()
|
||||
|
||||
self.activateAreaNode = AccessibilityAreaNode()
|
||||
@ -248,7 +272,8 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
if animateCheckNode {
|
||||
self.checkNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
self.gradientNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.leftShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.rightShadowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.typeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.durationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.draftNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
@ -322,10 +347,10 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
if draft.isVideo {
|
||||
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
|
||||
|
||||
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(12.0), textColor: .white)
|
||||
self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(draft.duration ?? 0.0)), font: Font.semibold(11.0), textColor: .white)
|
||||
|
||||
if self.typeIconNode.supernode == nil {
|
||||
self.addSubnode(self.gradientNode)
|
||||
self.addSubnode(self.rightShadowNode)
|
||||
self.addSubnode(self.typeIconNode)
|
||||
self.addSubnode(self.durationNode)
|
||||
self.setNeedsLayout()
|
||||
@ -337,8 +362,11 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
if self.durationNode.supernode != nil {
|
||||
self.durationNode.removeFromSupernode()
|
||||
}
|
||||
if self.gradientNode.supernode != nil {
|
||||
self.gradientNode.removeFromSupernode()
|
||||
if self.leftShadowNode.supernode != nil {
|
||||
self.leftShadowNode.removeFromSupernode()
|
||||
}
|
||||
if self.rightShadowNode.supernode != nil {
|
||||
self.rightShadowNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
@ -537,12 +565,20 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
duration = stringForDuration(Int32(asset.duration))
|
||||
}
|
||||
|
||||
if typeIcon != nil || duration != nil {
|
||||
if self.gradientNode.supernode == nil {
|
||||
self.addSubnode(self.gradientNode)
|
||||
if typeIcon != nil {
|
||||
if self.leftShadowNode.supernode == nil {
|
||||
self.addSubnode(self.leftShadowNode)
|
||||
}
|
||||
} else if self.gradientNode.supernode != nil {
|
||||
self.gradientNode.removeFromSupernode()
|
||||
} else if self.leftShadowNode.supernode != nil {
|
||||
self.leftShadowNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if duration != nil {
|
||||
if self.rightShadowNode.supernode == nil {
|
||||
self.addSubnode(self.rightShadowNode)
|
||||
}
|
||||
} else if self.rightShadowNode.supernode != nil {
|
||||
self.rightShadowNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let typeIcon {
|
||||
@ -555,7 +591,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
if let duration {
|
||||
self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(12.0), textColor: .white)
|
||||
self.durationNode.attributedText = NSAttributedString(string: duration, font: Font.semibold(11.0), textColor: .white)
|
||||
if self.durationNode.supernode == nil {
|
||||
self.addSubnode(self.durationNode)
|
||||
}
|
||||
@ -608,13 +644,14 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
let backgroundSize = CGSize(width: self.bounds.width, height: floorToScreenPixels(self.bounds.height / 9.0 * 16.0))
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((self.bounds.height - backgroundSize.height) / 2.0)), size: backgroundSize)
|
||||
self.imageNode.frame = self.bounds
|
||||
self.gradientNode.frame = CGRect(x: 0.0, y: self.bounds.height - 36.0, width: self.bounds.width, height: 36.0)
|
||||
self.leftShadowNode.frame = CGRect(x: 0.0, y: self.bounds.height - leftShadowImage.size.height, width: min(leftShadowImage.size.width, self.bounds.width), height: leftShadowImage.size.height)
|
||||
self.rightShadowNode.frame = CGRect(x: self.bounds.width - min(rightShadowImage.size.width, self.bounds.width), y: self.bounds.height - rightShadowImage.size.height, width: min(rightShadowImage.size.width, self.bounds.width), height: rightShadowImage.size.height)
|
||||
self.typeIconNode.frame = CGRect(x: 0.0, y: self.bounds.height - 20.0, width: 19.0, height: 19.0)
|
||||
self.activateAreaNode.frame = self.bounds
|
||||
|
||||
if self.durationNode.supernode != nil {
|
||||
let durationSize = self.durationNode.updateLayout(self.bounds.size)
|
||||
self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 7.0, y: self.bounds.height - durationSize.height - 5.0), size: durationSize)
|
||||
self.durationNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width - durationSize.width - 6.0, y: self.bounds.height - durationSize.height - 6.0), size: durationSize)
|
||||
}
|
||||
|
||||
if self.draftNode.supernode != nil {
|
||||
|
@ -34,6 +34,7 @@ swift_library(
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,6 +13,7 @@ import AccountContext
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ShimmerEffect
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
||||
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,57 +1,7 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","]
|
||||
private func decodeStickerThumbnailData(_ data: Data) -> String {
|
||||
var string = "M"
|
||||
data.forEach { byte in
|
||||
if byte >= 128 + 64 {
|
||||
string.append(decodingMap[Int(byte) - 128 - 64])
|
||||
} else {
|
||||
if byte >= 128 {
|
||||
string.append(",")
|
||||
} else if byte >= 64 {
|
||||
string.append("-")
|
||||
}
|
||||
string.append("\(byte & 63)")
|
||||
}
|
||||
}
|
||||
string.append("z")
|
||||
return string
|
||||
}
|
||||
|
||||
public func generateStickerPlaceholderImage(data: Data?, size: CGSize, scale: CGFloat? = nil, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(size, scale: scale, rotatedContext: { size, context in
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
} else {
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
var path = decodeStickerThumbnailData(data)
|
||||
if !path.hasSuffix("z") {
|
||||
path = "\(path)z"
|
||||
}
|
||||
let reader = PathDataReader(input: path)
|
||||
let segments = reader.read()
|
||||
|
||||
let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height)
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
renderPath(segments, context: context)
|
||||
} else {
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0))
|
||||
UIGraphicsPushContext(context)
|
||||
path.fill()
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
})
|
||||
}
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
||||
public class StickerShimmerEffectNode: ASDisplayNode {
|
||||
private var backdropNode: ASDisplayNode?
|
||||
@ -155,512 +105,3 @@ public class StickerShimmerEffectNode: ASDisplayNode {
|
||||
self.effectNode.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
open class PathSegment: Equatable {
|
||||
public enum SegmentType {
|
||||
case M
|
||||
case L
|
||||
case C
|
||||
case Q
|
||||
case A
|
||||
case z
|
||||
case H
|
||||
case V
|
||||
case S
|
||||
case T
|
||||
case m
|
||||
case l
|
||||
case c
|
||||
case q
|
||||
case a
|
||||
case h
|
||||
case v
|
||||
case s
|
||||
case t
|
||||
case E
|
||||
case e
|
||||
}
|
||||
|
||||
public let type: SegmentType
|
||||
public let data: [Double]
|
||||
|
||||
public init(type: PathSegment.SegmentType = .M, data: [Double] = []) {
|
||||
self.type = type
|
||||
self.data = data
|
||||
}
|
||||
|
||||
open func isAbsolute() -> Bool {
|
||||
switch type {
|
||||
case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool {
|
||||
return lhs.type == rhs.type && lhs.data == rhs.data
|
||||
}
|
||||
}
|
||||
|
||||
private func renderPath(_ segments: [PathSegment], context: CGContext) {
|
||||
var currentPoint: CGPoint?
|
||||
var cubicPoint: CGPoint?
|
||||
var quadrPoint: CGPoint?
|
||||
var initialPoint: CGPoint?
|
||||
|
||||
func M(_ x: Double, y: Double) {
|
||||
let point = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
context.move(to: point)
|
||||
setInitPoint(point)
|
||||
}
|
||||
|
||||
func m(_ x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
context.move(to: next)
|
||||
setInitPoint(next)
|
||||
} else {
|
||||
M(x, y: y)
|
||||
}
|
||||
}
|
||||
|
||||
func L(_ x: Double, y: Double) {
|
||||
lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y)))
|
||||
}
|
||||
|
||||
func l(_ x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y))
|
||||
} else {
|
||||
L(x, y: y)
|
||||
}
|
||||
}
|
||||
|
||||
func H(_ x: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y)))
|
||||
}
|
||||
}
|
||||
|
||||
func h(_ x: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y)))
|
||||
}
|
||||
}
|
||||
|
||||
func V(_ y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y)))
|
||||
}
|
||||
}
|
||||
|
||||
func v(_ y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y))
|
||||
}
|
||||
}
|
||||
|
||||
func lineTo(_ p: CGPoint) {
|
||||
context.addLine(to: p)
|
||||
setPoint(p)
|
||||
}
|
||||
|
||||
func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y)
|
||||
let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y)
|
||||
context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2)
|
||||
setCubicPoint(endPoint, cubic: controlPoint2)
|
||||
}
|
||||
}
|
||||
|
||||
func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) {
|
||||
let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1))
|
||||
let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2))
|
||||
context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2)
|
||||
setCubicPoint(endPoint, cubic: controlPoint2)
|
||||
}
|
||||
|
||||
func s(_ x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y)
|
||||
let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
|
||||
let xy1: CGPoint
|
||||
if let curCubicVal = cubicPoint {
|
||||
xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y)
|
||||
} else {
|
||||
xy1 = cur
|
||||
}
|
||||
context.addCurve(to: next, control1: xy1, control2: nextCubic)
|
||||
setCubicPoint(next, cubic: nextCubic)
|
||||
}
|
||||
}
|
||||
|
||||
func S(_ x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2))
|
||||
let next = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
let xy1: CGPoint
|
||||
if let curCubicVal = cubicPoint {
|
||||
xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y)
|
||||
} else {
|
||||
xy1 = cur
|
||||
}
|
||||
context.addCurve(to: next, control1: xy1, control2: nextCubic)
|
||||
setCubicPoint(next, cubic: nextCubic)
|
||||
}
|
||||
}
|
||||
|
||||
func z() {
|
||||
context.fillPath()
|
||||
}
|
||||
|
||||
func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) {
|
||||
currentPoint = p
|
||||
quadrPoint = quadr
|
||||
cubicPoint = nil
|
||||
}
|
||||
|
||||
func setCubicPoint(_ p: CGPoint, cubic: CGPoint) {
|
||||
currentPoint = p
|
||||
cubicPoint = cubic
|
||||
quadrPoint = nil
|
||||
}
|
||||
|
||||
func setInitPoint(_ p: CGPoint) {
|
||||
setPoint(p)
|
||||
initialPoint = p
|
||||
}
|
||||
|
||||
func setPoint(_ p: CGPoint) {
|
||||
currentPoint = p
|
||||
cubicPoint = nil
|
||||
quadrPoint = nil
|
||||
}
|
||||
|
||||
let _ = initialPoint
|
||||
let _ = quadrPoint
|
||||
|
||||
for segment in segments {
|
||||
var data = segment.data
|
||||
switch segment.type {
|
||||
case .M:
|
||||
M(data[0], y: data[1])
|
||||
data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2)))
|
||||
while data.count >= 2 {
|
||||
L(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .m:
|
||||
m(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
while data.count >= 2 {
|
||||
l(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .L:
|
||||
while data.count >= 2 {
|
||||
L(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .l:
|
||||
while data.count >= 2 {
|
||||
l(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .H:
|
||||
H(data[0])
|
||||
case .h:
|
||||
h(data[0])
|
||||
case .V:
|
||||
V(data[0])
|
||||
case .v:
|
||||
v(data[0])
|
||||
case .C:
|
||||
while data.count >= 6 {
|
||||
C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5])
|
||||
data.removeSubrange((0 ..< 6))
|
||||
}
|
||||
case .c:
|
||||
while data.count >= 6 {
|
||||
c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5])
|
||||
data.removeSubrange((0 ..< 6))
|
||||
}
|
||||
case .S:
|
||||
while data.count >= 4 {
|
||||
S(data[0], y2: data[1], x: data[2], y: data[3])
|
||||
data.removeSubrange((0 ..< 4))
|
||||
}
|
||||
case .s:
|
||||
while data.count >= 4 {
|
||||
s(data[0], y2: data[1], x: data[2], y: data[3])
|
||||
data.removeSubrange((0 ..< 4))
|
||||
}
|
||||
case .z:
|
||||
z()
|
||||
default:
|
||||
print("unknown")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PathDataReader {
|
||||
private let input: String
|
||||
private var current: UnicodeScalar?
|
||||
private var previous: UnicodeScalar?
|
||||
private var iterator: String.UnicodeScalarView.Iterator
|
||||
|
||||
private static let spaces: Set<UnicodeScalar> = Set("\n\r\t ,".unicodeScalars)
|
||||
|
||||
init(input: String) {
|
||||
self.input = input
|
||||
self.iterator = input.unicodeScalars.makeIterator()
|
||||
}
|
||||
|
||||
public func read() -> [PathSegment] {
|
||||
readNext()
|
||||
var segments = [PathSegment]()
|
||||
while let array = readSegments() {
|
||||
segments.append(contentsOf: array)
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
private func readSegments() -> [PathSegment]? {
|
||||
if let type = readSegmentType() {
|
||||
let argCount = getArgCount(segment: type)
|
||||
if argCount == 0 {
|
||||
return [PathSegment(type: type)]
|
||||
}
|
||||
var result = [PathSegment]()
|
||||
let data: [Double]
|
||||
if type == .a || type == .A {
|
||||
data = readDataOfASegment()
|
||||
} else {
|
||||
data = readData()
|
||||
}
|
||||
var index = 0
|
||||
var isFirstSegment = true
|
||||
while index < data.count {
|
||||
let end = index + argCount
|
||||
if end > data.count {
|
||||
break
|
||||
}
|
||||
var currentType = type
|
||||
if type == .M && !isFirstSegment {
|
||||
currentType = .L
|
||||
}
|
||||
if type == .m && !isFirstSegment {
|
||||
currentType = .l
|
||||
}
|
||||
result.append(PathSegment(type: currentType, data: Array(data[index..<end])))
|
||||
isFirstSegment = false
|
||||
index = end
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func readData() -> [Double] {
|
||||
var data = [Double]()
|
||||
while true {
|
||||
skipSpaces()
|
||||
if let value = readNum() {
|
||||
data.append(value)
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readDataOfASegment() -> [Double] {
|
||||
let argCount = getArgCount(segment: .A)
|
||||
var data: [Double] = []
|
||||
var index = 0
|
||||
while true {
|
||||
skipSpaces()
|
||||
let value: Double?
|
||||
let indexMod = index % argCount
|
||||
if indexMod == 3 || indexMod == 4 {
|
||||
value = readFlag()
|
||||
} else {
|
||||
value = readNum()
|
||||
}
|
||||
guard let doubleValue = value else {
|
||||
return data
|
||||
}
|
||||
data.append(doubleValue)
|
||||
index += 1
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private func skipSpaces() {
|
||||
var currentCharacter = current
|
||||
while let character = currentCharacter, Self.spaces.contains(character) {
|
||||
currentCharacter = readNext()
|
||||
}
|
||||
}
|
||||
|
||||
private func readFlag() -> Double? {
|
||||
guard let ch = current else {
|
||||
return .none
|
||||
}
|
||||
readNext()
|
||||
switch ch {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func readNum() -> Double? {
|
||||
guard let ch = current else {
|
||||
return .none
|
||||
}
|
||||
|
||||
guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else {
|
||||
return .none
|
||||
}
|
||||
|
||||
var chars = [ch]
|
||||
var hasDot = ch == "."
|
||||
while let ch = readDigit(&hasDot) {
|
||||
chars.append(ch)
|
||||
}
|
||||
|
||||
var buf = ""
|
||||
buf.unicodeScalars.append(contentsOf: chars)
|
||||
guard let value = Double(buf) else {
|
||||
return .none
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? {
|
||||
if let ch = readNext() {
|
||||
if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") {
|
||||
return ch
|
||||
} else if ch == "." && !hasDot {
|
||||
hasDot = true
|
||||
return ch
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool {
|
||||
switch ch {
|
||||
case "0"..."9":
|
||||
return true
|
||||
case ".":
|
||||
if hasDot {
|
||||
return false
|
||||
}
|
||||
hasDot = true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func readNext() -> UnicodeScalar? {
|
||||
previous = current
|
||||
current = iterator.next()
|
||||
return current
|
||||
}
|
||||
|
||||
private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool {
|
||||
if let ch = ch {
|
||||
return "\n\r\t ,".contains(String(ch))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func readSegmentType() -> PathSegment.SegmentType? {
|
||||
while true {
|
||||
if let type = getPathSegmentType() {
|
||||
readNext()
|
||||
return type
|
||||
}
|
||||
if readNext() == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getPathSegmentType() -> PathSegment.SegmentType? {
|
||||
if let ch = current {
|
||||
switch ch {
|
||||
case "M":
|
||||
return .M
|
||||
case "m":
|
||||
return .m
|
||||
case "L":
|
||||
return .L
|
||||
case "l":
|
||||
return .l
|
||||
case "C":
|
||||
return .C
|
||||
case "c":
|
||||
return .c
|
||||
case "Q":
|
||||
return .Q
|
||||
case "q":
|
||||
return .q
|
||||
case "A":
|
||||
return .A
|
||||
case "a":
|
||||
return .a
|
||||
case "z", "Z":
|
||||
return .z
|
||||
case "H":
|
||||
return .H
|
||||
case "h":
|
||||
return .h
|
||||
case "V":
|
||||
return .V
|
||||
case "v":
|
||||
return .v
|
||||
case "S":
|
||||
return .S
|
||||
case "s":
|
||||
return .s
|
||||
case "T":
|
||||
return .T
|
||||
case "t":
|
||||
return .t
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int {
|
||||
switch segment {
|
||||
case .H, .h, .V, .v:
|
||||
return 1
|
||||
case .M, .m, .L, .l, .T, .t:
|
||||
return 2
|
||||
case .S, .s, .Q, .q:
|
||||
return 4
|
||||
case .C, .c:
|
||||
return 6
|
||||
case .A, .a:
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1276,7 +1276,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
icon: itemFile.isPremiumSticker ? .premium : .none,
|
||||
tintMode: animationData.isTemplate ? .primary : .none
|
||||
)
|
||||
items.append(item)
|
||||
|
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -16,6 +16,7 @@ import MultiAnimationRenderer
|
||||
import ShimmerEffect
|
||||
import TextFormat
|
||||
import TelegramUIPreferences
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
||||
public func generateTopicIcon(title: String, backgroundColors: [UIColor], strokeColors: [UIColor], size: CGSize) -> UIImage? {
|
||||
let realSize = size
|
||||
|
@ -46,6 +46,7 @@ swift_library(
|
||||
"//submodules/GZip",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -24,6 +24,7 @@ import SolidRoundedButtonComponent
|
||||
import EmojiTextAttachmentView
|
||||
import EmojiStatusComponent
|
||||
import TelegramNotices
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
||||
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
|
||||
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
|
||||
|
@ -43,10 +43,10 @@ private final class LottieDirectContent: LottieComponent.Content {
|
||||
return true
|
||||
}
|
||||
|
||||
override func load(_ f: @escaping (Data, String?) -> Void) -> Disposable {
|
||||
override func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) {
|
||||
let result = TGGUnzipData(data, 2 * 1024 * 1024) ?? data
|
||||
f(result, nil)
|
||||
f(.animation(data: result, cacheKey: nil))
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
|
@ -17,6 +17,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/GZip",
|
||||
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -7,10 +7,16 @@ import RLottieBinding
|
||||
import SwiftSignalKit
|
||||
import AppBundle
|
||||
import GZip
|
||||
import GenerateStickerPlaceholderImage
|
||||
|
||||
public final class LottieComponent: Component {
|
||||
public typealias EnvironmentType = Empty
|
||||
|
||||
public enum ContentData {
|
||||
case placeholder(data: Data)
|
||||
case animation(data: Data, cacheKey: String?)
|
||||
}
|
||||
|
||||
open class Content: Equatable {
|
||||
open var frameRange: Range<Double> {
|
||||
preconditionFailure()
|
||||
@ -30,7 +36,7 @@ public final class LottieComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
open func load(_ f: @escaping (Data, String?) -> Void) -> Disposable {
|
||||
open func load(_ f: @escaping (ContentData) -> Void) -> Disposable {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
@ -61,11 +67,11 @@ public final class LottieComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable {
|
||||
override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
|
||||
if let url = getAppBundle().url(forResource: self.name, withExtension: "json"), let data = try? Data(contentsOf: url) {
|
||||
f(data, url.path)
|
||||
f(.animation(data: data, cacheKey: url.path))
|
||||
} else if let url = getAppBundle().url(forResource: self.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
|
||||
f(unpackedData, url.path)
|
||||
f(.animation(data: unpackedData, cacheKey: url.path))
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
@ -80,6 +86,7 @@ public final class LottieComponent: Component {
|
||||
|
||||
public let content: Content
|
||||
public let color: UIColor?
|
||||
public let placeholderColor: UIColor?
|
||||
public let startingPosition: StartingPosition
|
||||
public let size: CGSize?
|
||||
public let renderingScale: CGFloat?
|
||||
@ -88,6 +95,7 @@ public final class LottieComponent: Component {
|
||||
public init(
|
||||
content: Content,
|
||||
color: UIColor? = nil,
|
||||
placeholderColor: UIColor? = nil,
|
||||
startingPosition: StartingPosition = .end,
|
||||
size: CGSize? = nil,
|
||||
renderingScale: CGFloat? = nil,
|
||||
@ -95,6 +103,7 @@ public final class LottieComponent: Component {
|
||||
) {
|
||||
self.content = content
|
||||
self.color = color
|
||||
self.placeholderColor = placeholderColor
|
||||
self.startingPosition = startingPosition
|
||||
self.size = size
|
||||
self.renderingScale = renderingScale
|
||||
@ -108,6 +117,9 @@ public final class LottieComponent: Component {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.placeholderColor != rhs.placeholderColor {
|
||||
return false
|
||||
}
|
||||
if lhs.startingPosition != rhs.startingPosition {
|
||||
return false
|
||||
}
|
||||
@ -272,6 +284,26 @@ public final class LottieComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPlaceholder(data: Data) {
|
||||
guard let component = self.component, let placeholderColor = component.placeholderColor else {
|
||||
return
|
||||
}
|
||||
guard let currentDisplaySize = self.currentDisplaySize else {
|
||||
return
|
||||
}
|
||||
|
||||
if let image = generateStickerPlaceholderImage(
|
||||
data: data,
|
||||
size: currentDisplaySize,
|
||||
scale: min(2.0, UIScreenScale),
|
||||
imageSize: CGSize(width: 512.0, height: 512.0),
|
||||
backgroundColor: nil,
|
||||
foregroundColor: placeholderColor
|
||||
) {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAnimation(data: Data, cacheKey: String?, startingPosition: StartingPosition, frameRange: Range<Double>) {
|
||||
self.animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: cacheKey ?? "")
|
||||
if let animationInstance = self.animationInstance {
|
||||
@ -395,12 +427,17 @@ public final class LottieComponent: Component {
|
||||
self.currentContentDisposable?.dispose()
|
||||
let content = component.content
|
||||
let frameRange = content.frameRange
|
||||
self.currentContentDisposable = component.content.load { [weak self, weak content] data, cacheKey in
|
||||
self.currentContentDisposable = component.content.load { [weak self, weak content] result in
|
||||
Queue.mainQueue().async {
|
||||
guard let self, let component = self.component, component.content == content else {
|
||||
return
|
||||
}
|
||||
self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition, frameRange: frameRange)
|
||||
switch result {
|
||||
case let .placeholder(data):
|
||||
self.loadPlaceholder(data: data)
|
||||
case let .animation(data, cacheKey):
|
||||
self.loadAnimation(data: data, cacheKey: cacheKey, startingPosition: component.startingPosition, frameRange: frameRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if redrawImage {
|
||||
|
@ -34,7 +34,7 @@ public extension LottieComponent {
|
||||
return true
|
||||
}
|
||||
|
||||
override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable {
|
||||
override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
|
||||
let fileId = self.fileId
|
||||
let mediaBox = self.context.account.postbox.mediaBox
|
||||
return (self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||
@ -64,7 +64,7 @@ public extension LottieComponent {
|
||||
guard let data else {
|
||||
return
|
||||
}
|
||||
f(data, nil)
|
||||
f(.animation(data: data, cacheKey: nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ public extension LottieComponent {
|
||||
private let context: AccountContext
|
||||
private let file: TelegramMediaFile
|
||||
private let attemptSynchronously: Bool
|
||||
private let providesPlaceholder: Bool
|
||||
|
||||
override public var frameRange: Range<Double> {
|
||||
return 0.0 ..< 1.0
|
||||
@ -18,11 +19,13 @@ public extension LottieComponent {
|
||||
public init(
|
||||
context: AccountContext,
|
||||
file: TelegramMediaFile,
|
||||
attemptSynchronously: Bool
|
||||
attemptSynchronously: Bool,
|
||||
providesPlaceholder: Bool = false
|
||||
) {
|
||||
self.context = context
|
||||
self.file = file
|
||||
self.attemptSynchronously = attemptSynchronously
|
||||
self.providesPlaceholder = providesPlaceholder
|
||||
|
||||
super.init()
|
||||
}
|
||||
@ -37,13 +40,17 @@ public extension LottieComponent {
|
||||
if self.attemptSynchronously != other.attemptSynchronously {
|
||||
return false
|
||||
}
|
||||
if self.providesPlaceholder != other.providesPlaceholder {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override public func load(_ f: @escaping (Data, String?) -> Void) -> Disposable {
|
||||
override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
|
||||
let attemptSynchronously = self.attemptSynchronously
|
||||
let file = self.file
|
||||
let mediaBox = self.context.account.postbox.mediaBox
|
||||
let providesPlaceholder = self.providesPlaceholder
|
||||
return Signal<Data?, NoError> { subscriber in
|
||||
if attemptSynchronously {
|
||||
if let path = mediaBox.completedResourcePath(file.resource), let contents = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||
@ -56,11 +63,15 @@ public extension LottieComponent {
|
||||
}
|
||||
|
||||
let dataDisposable = (mediaBox.resourceData(file.resource)
|
||||
|> filter { data in return data.complete }).start(next: { data in
|
||||
if let contents = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
let result = TGGUnzipData(contents, 2 * 1024 * 1024) ?? contents
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
).start(next: { data in
|
||||
if data.complete {
|
||||
if let contents = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {
|
||||
let result = TGGUnzipData(contents, 2 * 1024 * 1024) ?? contents
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putNext(nil)
|
||||
}
|
||||
} else {
|
||||
subscriber.putNext(nil)
|
||||
}
|
||||
@ -73,9 +84,12 @@ public extension LottieComponent {
|
||||
}
|
||||
}.start(next: { data in
|
||||
guard let data else {
|
||||
if providesPlaceholder, let immediateThumbnailData = file.immediateThumbnailData {
|
||||
f(.placeholder(data: immediateThumbnailData))
|
||||
}
|
||||
return
|
||||
}
|
||||
f(data, nil)
|
||||
f(.animation(data: data, cacheKey: nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,10 @@ public final class MediaEditor {
|
||||
private var player: AVPlayer?
|
||||
private var additionalPlayer: AVPlayer?
|
||||
private var audioPlayer: AVPlayer?
|
||||
|
||||
private var timeObserver: Any?
|
||||
private weak var timeObserverPlayer: AVPlayer?
|
||||
|
||||
private var didPlayToEndTimeObserver: NSObjectProtocol?
|
||||
|
||||
private weak var previewView: MediaEditorPreviewView?
|
||||
@ -354,12 +357,10 @@ public final class MediaEditor {
|
||||
|
||||
private func destroyTimeObservers() {
|
||||
if let timeObserver = self.timeObserver {
|
||||
if self.sourceIsVideo {
|
||||
self.player?.removeTimeObserver(timeObserver)
|
||||
} else {
|
||||
self.audioPlayer?.removeTimeObserver(timeObserver)
|
||||
}
|
||||
self.timeObserverPlayer?.removeTimeObserver(timeObserver)
|
||||
|
||||
self.timeObserver = nil
|
||||
self.timeObserverPlayer = nil
|
||||
}
|
||||
if let didPlayToEndTimeObserver = self.didPlayToEndTimeObserver {
|
||||
NotificationCenter.default.removeObserver(didPlayToEndTimeObserver)
|
||||
@ -616,6 +617,7 @@ public final class MediaEditor {
|
||||
}
|
||||
|
||||
if self.timeObserver == nil {
|
||||
self.timeObserverPlayer = observedPlayer
|
||||
self.timeObserver = observedPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self, weak observedPlayer] time in
|
||||
guard let self, let observedPlayer, let duration = observedPlayer.currentItem?.duration.seconds else {
|
||||
return
|
||||
|
@ -129,7 +129,10 @@ private struct Month: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private let durationFont = Font.regular(12.0)
|
||||
private let durationFont: UIFont = {
|
||||
Font.semibold(11.0)
|
||||
}()
|
||||
|
||||
private let minDurationImage: UIImage = {
|
||||
let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
@ -144,6 +147,46 @@ private let minDurationImage: UIImage = {
|
||||
return image!
|
||||
}()
|
||||
|
||||
private let leftShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: -1.0, y: 1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
private let rightShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
private let viewCountImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridViewCount")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
private final class DurationLayer: CALayer {
|
||||
override init() {
|
||||
super.init()
|
||||
@ -159,6 +202,42 @@ private final class DurationLayer: CALayer {
|
||||
override func action(forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
}
|
||||
|
||||
func update(viewCount: Int32, isMin: Bool) {
|
||||
if isMin {
|
||||
self.contents = nil
|
||||
} else {
|
||||
let countString: String
|
||||
if viewCount > 1000000 {
|
||||
countString = "\(viewCount / 1000000)M"
|
||||
} else if viewCount > 1000 {
|
||||
countString = "\(viewCount / 1000)K"
|
||||
} else {
|
||||
countString = "\(viewCount)"
|
||||
}
|
||||
let string = NSAttributedString(string: countString, font: durationFont, textColor: .white)
|
||||
let bounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let textSize = CGSize(width: ceil(bounds.width), height: ceil(bounds.height))
|
||||
let sideInset: CGFloat = 6.0
|
||||
let verticalInset: CGFloat = 2.0
|
||||
let iconSpacing: CGFloat = -3.0
|
||||
let image = generateImage(CGSize(width: viewCountImage.size.width + iconSpacing + textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
viewCountImage.draw(in: CGRect(origin: CGPoint(x: 0.0, y: (size.height - viewCountImage.size.height) * 0.5), size: viewCountImage.size))
|
||||
|
||||
string.draw(in: bounds.offsetBy(dx: sideInset + viewCountImage.size.width + iconSpacing, dy: verticalInset))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.contents = image?.cgImage
|
||||
}
|
||||
}
|
||||
|
||||
func update(duration: Int32, isMin: Bool) {
|
||||
if isMin {
|
||||
@ -171,14 +250,11 @@ private final class DurationLayer: CALayer {
|
||||
let verticalInset: CGFloat = 2.0
|
||||
let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
|
||||
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset))
|
||||
UIGraphicsPopContext()
|
||||
@ -198,7 +274,7 @@ private protocol ItemLayer: SparseItemGridLayer {
|
||||
var hasContents: Bool { get set }
|
||||
func setSpoilerContents(_ contents: Any?)
|
||||
|
||||
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat)
|
||||
func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat)
|
||||
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool)
|
||||
func updateHasSpoiler(hasSpoiler: Bool)
|
||||
|
||||
@ -208,7 +284,10 @@ private protocol ItemLayer: SparseItemGridLayer {
|
||||
|
||||
private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
var item: VisualMediaItem?
|
||||
var viewCountLayer: DurationLayer?
|
||||
var durationLayer: DurationLayer?
|
||||
var leftShadowLayer: SimpleLayer?
|
||||
var rightShadowLayer: SimpleLayer?
|
||||
var minFactor: CGFloat = 1.0
|
||||
var selectionLayer: GridMessageSelectionLayer?
|
||||
var dustLayer: MediaDustLayer?
|
||||
@ -254,17 +333,34 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
self.minFactor = minFactor
|
||||
|
||||
if let viewCount {
|
||||
if let viewCountLayer = self.viewCountLayer {
|
||||
viewCountLayer.update(viewCount: viewCount, isMin: isMin)
|
||||
} else {
|
||||
let viewCountLayer = DurationLayer()
|
||||
viewCountLayer.contentsGravity = .topLeft
|
||||
viewCountLayer.update(viewCount: viewCount, isMin: isMin)
|
||||
self.addSublayer(viewCountLayer)
|
||||
viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: self.bounds.height - 4.0), size: CGSize())
|
||||
viewCountLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0)
|
||||
self.viewCountLayer = viewCountLayer
|
||||
}
|
||||
} else if let viewCountLayer = self.viewCountLayer {
|
||||
self.viewCountLayer = nil
|
||||
viewCountLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if let duration = duration {
|
||||
if let duration {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
} else {
|
||||
let durationLayer = DurationLayer()
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
self.addSublayer(durationLayer)
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 3.0), size: CGSize())
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 4.0), size: CGSize())
|
||||
durationLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0)
|
||||
self.durationLayer = durationLayer
|
||||
}
|
||||
@ -272,6 +368,40 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
self.durationLayer = nil
|
||||
durationLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.viewCountLayer != nil {
|
||||
if self.leftShadowLayer == nil {
|
||||
let leftShadowLayer = SimpleLayer()
|
||||
self.leftShadowLayer = leftShadowLayer
|
||||
self.insertSublayer(leftShadowLayer, at: 0)
|
||||
leftShadowLayer.contents = leftShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height))
|
||||
leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let leftShadowLayer = self.leftShadowLayer {
|
||||
self.leftShadowLayer = nil
|
||||
leftShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
if self.durationLayer != nil {
|
||||
if self.rightShadowLayer == nil {
|
||||
let rightShadowLayer = SimpleLayer()
|
||||
self.rightShadowLayer = rightShadowLayer
|
||||
self.insertSublayer(rightShadowLayer, at: 0)
|
||||
rightShadowLayer.contents = rightShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
self.rightShadowLayer = nil
|
||||
rightShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) {
|
||||
@ -330,8 +460,21 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) {
|
||||
if let viewCountLayer = self.viewCountLayer {
|
||||
viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: size.height - 4.0), size: CGSize())
|
||||
}
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 4.0), size: CGSize())
|
||||
}
|
||||
|
||||
if let leftShadowLayer = self.leftShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height))
|
||||
leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
|
||||
if let binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item, previousItem.story.media.id != item.story.media.id {
|
||||
@ -342,7 +485,10 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
|
||||
private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemLayer {
|
||||
var item: VisualMediaItem?
|
||||
var viewCountLayer: DurationLayer?
|
||||
var durationLayer: DurationLayer?
|
||||
var leftShadowLayer: SimpleLayer?
|
||||
var rightShadowLayer: SimpleLayer?
|
||||
var minFactor: CGFloat = 1.0
|
||||
var selectionLayer: GridMessageSelectionLayer?
|
||||
var dustLayer: MediaDustLayer?
|
||||
@ -398,17 +544,34 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
self.item = item
|
||||
}
|
||||
|
||||
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
func updateDuration(viewCount: Int32?, duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
self.minFactor = minFactor
|
||||
|
||||
if let duration = duration {
|
||||
if let viewCount {
|
||||
if let viewCountLayer = self.viewCountLayer {
|
||||
viewCountLayer.update(viewCount: viewCount, isMin: isMin)
|
||||
} else {
|
||||
let viewCountLayer = DurationLayer()
|
||||
viewCountLayer.contentsGravity = .topLeft
|
||||
viewCountLayer.update(viewCount: viewCount, isMin: isMin)
|
||||
self.addSublayer(viewCountLayer)
|
||||
viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: self.bounds.height - 4.0), size: CGSize())
|
||||
viewCountLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0)
|
||||
self.viewCountLayer = viewCountLayer
|
||||
}
|
||||
} else if let viewCountLayer = self.viewCountLayer {
|
||||
self.viewCountLayer = nil
|
||||
viewCountLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if let duration {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
} else {
|
||||
let durationLayer = DurationLayer()
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
self.addSublayer(durationLayer)
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 3.0), size: CGSize())
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: self.bounds.width - 3.0, y: self.bounds.height - 4.0), size: CGSize())
|
||||
durationLayer.transform = CATransform3DMakeScale(minFactor, minFactor, 1.0)
|
||||
self.durationLayer = durationLayer
|
||||
}
|
||||
@ -416,6 +579,40 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
self.durationLayer = nil
|
||||
durationLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.viewCountLayer != nil {
|
||||
if self.leftShadowLayer == nil {
|
||||
let leftShadowLayer = SimpleLayer()
|
||||
self.leftShadowLayer = leftShadowLayer
|
||||
self.insertSublayer(leftShadowLayer, at: 0)
|
||||
leftShadowLayer.contents = leftShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height))
|
||||
leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let leftShadowLayer = self.leftShadowLayer {
|
||||
self.leftShadowLayer = nil
|
||||
leftShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
if self.durationLayer != nil {
|
||||
if self.rightShadowLayer == nil {
|
||||
let rightShadowLayer = SimpleLayer()
|
||||
self.rightShadowLayer = rightShadowLayer
|
||||
self.insertSublayer(rightShadowLayer, at: 0)
|
||||
rightShadowLayer.contents = rightShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
self.rightShadowLayer = nil
|
||||
rightShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) {
|
||||
@ -474,8 +671,21 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) {
|
||||
if let viewCountLayer = self.viewCountLayer {
|
||||
viewCountLayer.frame = CGRect(origin: CGPoint(x: 7.0, y: size.height - 4.0), size: CGSize())
|
||||
}
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 4.0), size: CGSize())
|
||||
}
|
||||
|
||||
if let leftShadowLayer = self.leftShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, leftShadowImage.size.width), height: min(size.height, leftShadowImage.size.height))
|
||||
leftShadowLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -483,7 +693,11 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
private final class ItemTransitionView: UIView {
|
||||
private weak var itemLayer: CALayer?
|
||||
private var copyDurationLayer: SimpleLayer?
|
||||
private var copyViewCountLayer: SimpleLayer?
|
||||
private var copyLeftShadowLayer: SimpleLayer?
|
||||
private var copyRightShadowLayer: SimpleLayer?
|
||||
|
||||
private var viewCountLayerBottomLeftPosition: CGPoint?
|
||||
private var durationLayerBottomLeftPosition: CGPoint?
|
||||
|
||||
init(itemLayer: CALayer?) {
|
||||
@ -494,15 +708,57 @@ private final class ItemTransitionView: UIView {
|
||||
if let itemLayer {
|
||||
self.layer.contentsRect = itemLayer.contentsRect
|
||||
|
||||
var viewCountLayer: CALayer?
|
||||
var durationLayer: CALayer?
|
||||
var leftShadowLayer: CALayer?
|
||||
var rightShadowLayer: CALayer?
|
||||
if let itemLayer = itemLayer as? CaptureProtectedItemLayer {
|
||||
viewCountLayer = itemLayer.viewCountLayer
|
||||
durationLayer = itemLayer.durationLayer
|
||||
self.layer.contents = itemLayer.getContents()
|
||||
} else if let itemLayer = itemLayer as? ItemLayer {
|
||||
} else if let itemLayer = itemLayer as? GenericItemLayer {
|
||||
viewCountLayer = itemLayer.viewCountLayer
|
||||
durationLayer = itemLayer.durationLayer
|
||||
leftShadowLayer = itemLayer.leftShadowLayer
|
||||
rightShadowLayer = itemLayer.rightShadowLayer
|
||||
self.layer.contents = itemLayer.contents
|
||||
}
|
||||
|
||||
if let leftShadowLayer {
|
||||
let copyLayer = SimpleLayer()
|
||||
copyLayer.contents = leftShadowLayer.contents
|
||||
copyLayer.contentsRect = leftShadowLayer.contentsRect
|
||||
copyLayer.contentsGravity = leftShadowLayer.contentsGravity
|
||||
copyLayer.contentsScale = leftShadowLayer.contentsScale
|
||||
copyLayer.frame = leftShadowLayer.frame
|
||||
self.layer.addSublayer(copyLayer)
|
||||
self.copyLeftShadowLayer = copyLayer
|
||||
}
|
||||
|
||||
if let rightShadowLayer {
|
||||
let copyLayer = SimpleLayer()
|
||||
copyLayer.contents = rightShadowLayer.contents
|
||||
copyLayer.contentsRect = rightShadowLayer.contentsRect
|
||||
copyLayer.contentsGravity = rightShadowLayer.contentsGravity
|
||||
copyLayer.contentsScale = rightShadowLayer.contentsScale
|
||||
copyLayer.frame = rightShadowLayer.frame
|
||||
self.layer.addSublayer(copyLayer)
|
||||
self.copyRightShadowLayer = copyLayer
|
||||
}
|
||||
|
||||
if let viewCountLayer {
|
||||
let copyViewCountLayer = SimpleLayer()
|
||||
copyViewCountLayer.contents = viewCountLayer.contents
|
||||
copyViewCountLayer.contentsRect = viewCountLayer.contentsRect
|
||||
copyViewCountLayer.contentsGravity = viewCountLayer.contentsGravity
|
||||
copyViewCountLayer.contentsScale = viewCountLayer.contentsScale
|
||||
copyViewCountLayer.frame = viewCountLayer.frame
|
||||
self.layer.addSublayer(copyViewCountLayer)
|
||||
self.copyViewCountLayer = copyViewCountLayer
|
||||
|
||||
self.viewCountLayerBottomLeftPosition = CGPoint(x: viewCountLayer.frame.minX, y: itemLayer.bounds.height - viewCountLayer.frame.maxY)
|
||||
}
|
||||
|
||||
if let durationLayer {
|
||||
let copyDurationLayer = SimpleLayer()
|
||||
copyDurationLayer.contents = durationLayer.contents
|
||||
@ -528,6 +784,18 @@ private final class ItemTransitionView: UIView {
|
||||
if let copyDurationLayer = self.copyDurationLayer, let durationLayerBottomLeftPosition = self.durationLayerBottomLeftPosition {
|
||||
transition.setFrame(layer: copyDurationLayer, frame: CGRect(origin: CGPoint(x: size.width - durationLayerBottomLeftPosition.x - copyDurationLayer.bounds.width, y: size.height - durationLayerBottomLeftPosition.y - copyDurationLayer.bounds.height), size: copyDurationLayer.bounds.size))
|
||||
}
|
||||
|
||||
if let copyViewCountLayer = self.copyViewCountLayer, let viewcountLayerBottomLeftPosition = self.viewCountLayerBottomLeftPosition {
|
||||
transition.setFrame(layer: copyViewCountLayer, frame: CGRect(origin: CGPoint(x: viewcountLayerBottomLeftPosition.x, y: size.height - viewcountLayerBottomLeftPosition.y - copyViewCountLayer.bounds.height), size: copyViewCountLayer.bounds.size))
|
||||
}
|
||||
|
||||
if let copyLeftShadowLayer = self.copyLeftShadowLayer {
|
||||
transition.setFrame(layer: copyLeftShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - copyLeftShadowLayer.bounds.height), size: copyLeftShadowLayer.bounds.size))
|
||||
}
|
||||
|
||||
if let copyRightShadowLayer = self.copyRightShadowLayer {
|
||||
transition.setFrame(layer: copyRightShadowLayer, frame: CGRect(origin: CGPoint(x: size.width - copyRightShadowLayer.bounds.width, y: size.height - copyRightShadowLayer.bounds.height), size: copyRightShadowLayer.bounds.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -728,6 +996,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
}
|
||||
}
|
||||
|
||||
var viewCount: Int32?
|
||||
if let value = story.views?.seenCount {
|
||||
viewCount = Int32(value)
|
||||
}
|
||||
|
||||
var duration: Int32?
|
||||
var isMin: Bool = false
|
||||
if let file = selectedMedia as? TelegramMediaFile, !file.isAnimated {
|
||||
@ -736,7 +1009,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
}
|
||||
isMin = layer.bounds.width < 80.0
|
||||
}
|
||||
layer.updateDuration(duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
|
||||
layer.updateDuration(viewCount: viewCount, duration: duration, isMin: isMin, minFactor: min(1.0, layer.bounds.height / 74.0))
|
||||
}
|
||||
|
||||
var isSelected: Bool?
|
||||
|
@ -275,7 +275,7 @@ private struct Month: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private let durationFont = Font.regular(12.0)
|
||||
private let durationFont = Font.semibold(11.0)
|
||||
private let minDurationImage: UIImage = {
|
||||
let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
@ -290,6 +290,18 @@ private let minDurationImage: UIImage = {
|
||||
return image!
|
||||
}()
|
||||
|
||||
private let rightShadowImage: UIImage = {
|
||||
let baseImage = UIImage(bundleImageName: "Peer Info/MediaGridShadow")!
|
||||
let image = generateImage(baseImage.size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
baseImage.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
return image!
|
||||
}()
|
||||
|
||||
private final class DurationLayer: CALayer {
|
||||
override init() {
|
||||
super.init()
|
||||
@ -317,14 +329,11 @@ private final class DurationLayer: CALayer {
|
||||
let verticalInset: CGFloat = 2.0
|
||||
let image = generateImage(CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height + verticalInset * 2.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(white: 0.0, alpha: 0.5).cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
|
||||
|
||||
|
||||
context.setBlendMode(.normal)
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.22).cgColor)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
string.draw(in: bounds.offsetBy(dx: sideInset, dy: verticalInset))
|
||||
UIGraphicsPopContext()
|
||||
@ -355,6 +364,7 @@ private protocol ItemLayer: SparseItemGridLayer {
|
||||
private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
var item: VisualMediaItem?
|
||||
var durationLayer: DurationLayer?
|
||||
var rightShadowLayer: SimpleLayer?
|
||||
var minFactor: CGFloat = 1.0
|
||||
var selectionLayer: GridMessageSelectionLayer?
|
||||
var dustLayer: MediaDustLayer?
|
||||
@ -403,7 +413,7 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
self.minFactor = minFactor
|
||||
|
||||
if let duration = duration {
|
||||
if let duration {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
} else {
|
||||
@ -418,6 +428,24 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
self.durationLayer = nil
|
||||
durationLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.durationLayer != nil {
|
||||
if self.rightShadowLayer == nil {
|
||||
let rightShadowLayer = SimpleLayer()
|
||||
self.rightShadowLayer = rightShadowLayer
|
||||
self.insertSublayer(rightShadowLayer, at: 0)
|
||||
rightShadowLayer.contents = rightShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
self.rightShadowLayer = nil
|
||||
rightShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) {
|
||||
@ -476,15 +504,21 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) {
|
||||
/*if let durationLayer = self.durationLayer {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
||||
}*/
|
||||
}
|
||||
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemLayer {
|
||||
var item: VisualMediaItem?
|
||||
var durationLayer: DurationLayer?
|
||||
var rightShadowLayer: SimpleLayer?
|
||||
var minFactor: CGFloat = 1.0
|
||||
var selectionLayer: GridMessageSelectionLayer?
|
||||
var dustLayer: MediaDustLayer?
|
||||
@ -543,7 +577,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) {
|
||||
self.minFactor = minFactor
|
||||
|
||||
if let duration = duration {
|
||||
if let duration {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.update(duration: duration, isMin: isMin)
|
||||
} else {
|
||||
@ -558,6 +592,24 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
self.durationLayer = nil
|
||||
durationLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
if self.durationLayer != nil {
|
||||
if self.rightShadowLayer == nil {
|
||||
let rightShadowLayer = SimpleLayer()
|
||||
self.rightShadowLayer = rightShadowLayer
|
||||
self.insertSublayer(rightShadowLayer, at: 0)
|
||||
rightShadowLayer.contents = rightShadowImage.cgImage
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
} else {
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
self.rightShadowLayer = nil
|
||||
rightShadowLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) {
|
||||
@ -616,9 +668,14 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) {
|
||||
/*if let durationLayer = self.durationLayer {
|
||||
if let durationLayer = self.durationLayer {
|
||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
||||
}*/
|
||||
}
|
||||
|
||||
if let rightShadowLayer = self.rightShadowLayer {
|
||||
let shadowSize = CGSize(width: min(size.width, rightShadowImage.size.width), height: min(size.height, rightShadowImage.size.height))
|
||||
rightShadowLayer.frame = CGRect(origin: CGPoint(x: size.width - shadowSize.width, y: size.height - shadowSize.height), size: shadowSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.chatListNode = nil
|
||||
} else {
|
||||
self.mainContainerNode = nil
|
||||
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true)
|
||||
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
@ -448,6 +448,16 @@ final class StoryContentCaptionComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if case .tap = gesture {
|
||||
if component.externalState.isSelectingText {
|
||||
self.cancelTextSelection()
|
||||
} else if self.isExpanded {
|
||||
self.collapse(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
} else {
|
||||
self.expand(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -779,7 +779,7 @@ final class StoryItemContentComponent: Component {
|
||||
if let current = self.loadingEffectView {
|
||||
loadingEffectView = current
|
||||
} else {
|
||||
loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, duration: 1.0, hasCustomBorder: true, playOnce: false)
|
||||
loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, duration: 1.0, hasCustomBorder: false, playOnce: false)
|
||||
self.loadingEffectView = loadingEffectView
|
||||
self.addSubview(loadingEffectView)
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ import HierarchyTrackingLayer
|
||||
import ComponentFlow
|
||||
import Display
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Stories/PanelGradient")
|
||||
}()
|
||||
|
||||
final class StoryItemLoadingEffectView: UIView {
|
||||
private let duration: Double
|
||||
private let hasCustomBorder: Bool
|
||||
@ -46,16 +50,32 @@ final class StoryItemLoadingEffectView: UIView {
|
||||
|
||||
let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in
|
||||
return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
|
||||
let backgroundColor = UIColor.clear
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.clip(to: CGRect(origin: CGPoint(), size: size))
|
||||
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
|
||||
|
||||
let foregroundColor = UIColor(white: 1.0, alpha: baseAlpha)
|
||||
if let shadowImage {
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
|
||||
|
||||
context.saveGState()
|
||||
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
|
||||
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
|
||||
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
|
||||
|
||||
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fill(adjustedRect)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
/*
|
||||
let numColors = 7
|
||||
var locations: [CGFloat] = []
|
||||
var colors: [CGColor] = []
|
||||
@ -72,7 +92,7 @@ final class StoryItemLoadingEffectView: UIView {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())*/
|
||||
})
|
||||
}
|
||||
self.backgroundView.image = generateGradient(effectAlpha)
|
||||
@ -89,6 +109,11 @@ final class StoryItemLoadingEffectView: UIView {
|
||||
}
|
||||
|
||||
private func updateAnimations(size: CGSize) {
|
||||
/*if "".isEmpty {
|
||||
self.backgroundView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
|
||||
return
|
||||
}*/
|
||||
|
||||
if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) {
|
||||
return
|
||||
}
|
||||
|
@ -415,11 +415,13 @@ final class StoryItemOverlaysView: UIView {
|
||||
if file.isCustomTemplateEmoji {
|
||||
color = flags.contains(.isDark) ? .white : .black
|
||||
}
|
||||
let placeholderColor = flags.contains(.isDark) ? UIColor(white: 1.0, alpha: 0.1) : UIColor(white: 0.0, alpha: 0.1)
|
||||
let _ = directStickerView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous),
|
||||
content: LottieComponent.ResourceContent(context: context, file: file, attemptSynchronously: synchronous, providesPlaceholder: true),
|
||||
color: color,
|
||||
placeholderColor: placeholderColor,
|
||||
renderingScale: 2.0,
|
||||
loop: true
|
||||
)),
|
||||
|
@ -901,21 +901,30 @@ public final class TextFieldComponent: Component {
|
||||
if self.textView.inputView == nil {
|
||||
self.textView.inputView = inputView
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
// Avoid layout cycle
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if component.hideKeyboard {
|
||||
if self.textView.inputView == nil {
|
||||
self.textView.inputView = EmptyInputView()
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
// Avoid layout cycle
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.textView.inputView != nil {
|
||||
self.textView.inputView = nil
|
||||
if self.textView.isFirstResponder {
|
||||
self.textView.reloadInputViews()
|
||||
// Avoid layout cycle
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.textView.reloadInputViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GenerateStickerPlaceholderImage",
|
||||
module_name = "GenerateStickerPlaceholderImage",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,564 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
open class PathSegment: Equatable {
|
||||
public enum SegmentType {
|
||||
case M
|
||||
case L
|
||||
case C
|
||||
case Q
|
||||
case A
|
||||
case z
|
||||
case H
|
||||
case V
|
||||
case S
|
||||
case T
|
||||
case m
|
||||
case l
|
||||
case c
|
||||
case q
|
||||
case a
|
||||
case h
|
||||
case v
|
||||
case s
|
||||
case t
|
||||
case E
|
||||
case e
|
||||
}
|
||||
|
||||
public let type: SegmentType
|
||||
public let data: [Double]
|
||||
|
||||
public init(type: PathSegment.SegmentType = .M, data: [Double] = []) {
|
||||
self.type = type
|
||||
self.data = data
|
||||
}
|
||||
|
||||
open func isAbsolute() -> Bool {
|
||||
switch type {
|
||||
case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool {
|
||||
return lhs.type == rhs.type && lhs.data == rhs.data
|
||||
}
|
||||
}
|
||||
|
||||
private func renderPath(_ segments: [PathSegment], context: CGContext) {
|
||||
var currentPoint: CGPoint?
|
||||
var cubicPoint: CGPoint?
|
||||
var quadrPoint: CGPoint?
|
||||
var initialPoint: CGPoint?
|
||||
|
||||
func M(_ x: Double, y: Double) {
|
||||
let point = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
context.move(to: point)
|
||||
setInitPoint(point)
|
||||
}
|
||||
|
||||
func m(_ x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
context.move(to: next)
|
||||
setInitPoint(next)
|
||||
} else {
|
||||
M(x, y: y)
|
||||
}
|
||||
}
|
||||
|
||||
func L(_ x: Double, y: Double) {
|
||||
lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y)))
|
||||
}
|
||||
|
||||
func l(_ x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y))
|
||||
} else {
|
||||
L(x, y: y)
|
||||
}
|
||||
}
|
||||
|
||||
func H(_ x: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y)))
|
||||
}
|
||||
}
|
||||
|
||||
func h(_ x: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y)))
|
||||
}
|
||||
}
|
||||
|
||||
func V(_ y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y)))
|
||||
}
|
||||
}
|
||||
|
||||
func v(_ y: Double) {
|
||||
if let cur = currentPoint {
|
||||
lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y))
|
||||
}
|
||||
}
|
||||
|
||||
func lineTo(_ p: CGPoint) {
|
||||
context.addLine(to: p)
|
||||
setPoint(p)
|
||||
}
|
||||
|
||||
func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y)
|
||||
let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y)
|
||||
context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2)
|
||||
setCubicPoint(endPoint, cubic: controlPoint2)
|
||||
}
|
||||
}
|
||||
|
||||
func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) {
|
||||
let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1))
|
||||
let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2))
|
||||
context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2)
|
||||
setCubicPoint(endPoint, cubic: controlPoint2)
|
||||
}
|
||||
|
||||
func s(_ x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y)
|
||||
let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)
|
||||
|
||||
let xy1: CGPoint
|
||||
if let curCubicVal = cubicPoint {
|
||||
xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y)
|
||||
} else {
|
||||
xy1 = cur
|
||||
}
|
||||
context.addCurve(to: next, control1: xy1, control2: nextCubic)
|
||||
setCubicPoint(next, cubic: nextCubic)
|
||||
}
|
||||
}
|
||||
|
||||
func S(_ x2: Double, y2: Double, x: Double, y: Double) {
|
||||
if let cur = currentPoint {
|
||||
let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2))
|
||||
let next = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
let xy1: CGPoint
|
||||
if let curCubicVal = cubicPoint {
|
||||
xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y)
|
||||
} else {
|
||||
xy1 = cur
|
||||
}
|
||||
context.addCurve(to: next, control1: xy1, control2: nextCubic)
|
||||
setCubicPoint(next, cubic: nextCubic)
|
||||
}
|
||||
}
|
||||
|
||||
func z() {
|
||||
context.fillPath()
|
||||
}
|
||||
|
||||
func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) {
|
||||
currentPoint = p
|
||||
quadrPoint = quadr
|
||||
cubicPoint = nil
|
||||
}
|
||||
|
||||
func setCubicPoint(_ p: CGPoint, cubic: CGPoint) {
|
||||
currentPoint = p
|
||||
cubicPoint = cubic
|
||||
quadrPoint = nil
|
||||
}
|
||||
|
||||
func setInitPoint(_ p: CGPoint) {
|
||||
setPoint(p)
|
||||
initialPoint = p
|
||||
}
|
||||
|
||||
func setPoint(_ p: CGPoint) {
|
||||
currentPoint = p
|
||||
cubicPoint = nil
|
||||
quadrPoint = nil
|
||||
}
|
||||
|
||||
let _ = initialPoint
|
||||
let _ = quadrPoint
|
||||
|
||||
for segment in segments {
|
||||
var data = segment.data
|
||||
switch segment.type {
|
||||
case .M:
|
||||
M(data[0], y: data[1])
|
||||
data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2)))
|
||||
while data.count >= 2 {
|
||||
L(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .m:
|
||||
m(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
while data.count >= 2 {
|
||||
l(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .L:
|
||||
while data.count >= 2 {
|
||||
L(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .l:
|
||||
while data.count >= 2 {
|
||||
l(data[0], y: data[1])
|
||||
data.removeSubrange((0 ..< 2))
|
||||
}
|
||||
case .H:
|
||||
H(data[0])
|
||||
case .h:
|
||||
h(data[0])
|
||||
case .V:
|
||||
V(data[0])
|
||||
case .v:
|
||||
v(data[0])
|
||||
case .C:
|
||||
while data.count >= 6 {
|
||||
C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5])
|
||||
data.removeSubrange((0 ..< 6))
|
||||
}
|
||||
case .c:
|
||||
while data.count >= 6 {
|
||||
c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5])
|
||||
data.removeSubrange((0 ..< 6))
|
||||
}
|
||||
case .S:
|
||||
while data.count >= 4 {
|
||||
S(data[0], y2: data[1], x: data[2], y: data[3])
|
||||
data.removeSubrange((0 ..< 4))
|
||||
}
|
||||
case .s:
|
||||
while data.count >= 4 {
|
||||
s(data[0], y2: data[1], x: data[2], y: data[3])
|
||||
data.removeSubrange((0 ..< 4))
|
||||
}
|
||||
case .z:
|
||||
z()
|
||||
default:
|
||||
print("unknown")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PathDataReader {
|
||||
private let input: String
|
||||
private var current: UnicodeScalar?
|
||||
private var previous: UnicodeScalar?
|
||||
private var iterator: String.UnicodeScalarView.Iterator
|
||||
|
||||
private static let spaces: Set<UnicodeScalar> = Set("\n\r\t ,".unicodeScalars)
|
||||
|
||||
init(input: String) {
|
||||
self.input = input
|
||||
self.iterator = input.unicodeScalars.makeIterator()
|
||||
}
|
||||
|
||||
public func read() -> [PathSegment] {
|
||||
readNext()
|
||||
var segments = [PathSegment]()
|
||||
while let array = readSegments() {
|
||||
segments.append(contentsOf: array)
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
private func readSegments() -> [PathSegment]? {
|
||||
if let type = readSegmentType() {
|
||||
let argCount = getArgCount(segment: type)
|
||||
if argCount == 0 {
|
||||
return [PathSegment(type: type)]
|
||||
}
|
||||
var result = [PathSegment]()
|
||||
let data: [Double]
|
||||
if type == .a || type == .A {
|
||||
data = readDataOfASegment()
|
||||
} else {
|
||||
data = readData()
|
||||
}
|
||||
var index = 0
|
||||
var isFirstSegment = true
|
||||
while index < data.count {
|
||||
let end = index + argCount
|
||||
if end > data.count {
|
||||
break
|
||||
}
|
||||
var currentType = type
|
||||
if type == .M && !isFirstSegment {
|
||||
currentType = .L
|
||||
}
|
||||
if type == .m && !isFirstSegment {
|
||||
currentType = .l
|
||||
}
|
||||
result.append(PathSegment(type: currentType, data: Array(data[index..<end])))
|
||||
isFirstSegment = false
|
||||
index = end
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func readData() -> [Double] {
|
||||
var data = [Double]()
|
||||
while true {
|
||||
skipSpaces()
|
||||
if let value = readNum() {
|
||||
data.append(value)
|
||||
} else {
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readDataOfASegment() -> [Double] {
|
||||
let argCount = getArgCount(segment: .A)
|
||||
var data: [Double] = []
|
||||
var index = 0
|
||||
while true {
|
||||
skipSpaces()
|
||||
let value: Double?
|
||||
let indexMod = index % argCount
|
||||
if indexMod == 3 || indexMod == 4 {
|
||||
value = readFlag()
|
||||
} else {
|
||||
value = readNum()
|
||||
}
|
||||
guard let doubleValue = value else {
|
||||
return data
|
||||
}
|
||||
data.append(doubleValue)
|
||||
index += 1
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private func skipSpaces() {
|
||||
var currentCharacter = current
|
||||
while let character = currentCharacter, Self.spaces.contains(character) {
|
||||
currentCharacter = readNext()
|
||||
}
|
||||
}
|
||||
|
||||
private func readFlag() -> Double? {
|
||||
guard let ch = current else {
|
||||
return .none
|
||||
}
|
||||
readNext()
|
||||
switch ch {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func readNum() -> Double? {
|
||||
guard let ch = current else {
|
||||
return .none
|
||||
}
|
||||
|
||||
guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else {
|
||||
return .none
|
||||
}
|
||||
|
||||
var chars = [ch]
|
||||
var hasDot = ch == "."
|
||||
while let ch = readDigit(&hasDot) {
|
||||
chars.append(ch)
|
||||
}
|
||||
|
||||
var buf = ""
|
||||
buf.unicodeScalars.append(contentsOf: chars)
|
||||
guard let value = Double(buf) else {
|
||||
return .none
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? {
|
||||
if let ch = readNext() {
|
||||
if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") {
|
||||
return ch
|
||||
} else if ch == "." && !hasDot {
|
||||
hasDot = true
|
||||
return ch
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool {
|
||||
switch ch {
|
||||
case "0"..."9":
|
||||
return true
|
||||
case ".":
|
||||
if hasDot {
|
||||
return false
|
||||
}
|
||||
hasDot = true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func readNext() -> UnicodeScalar? {
|
||||
previous = current
|
||||
current = iterator.next()
|
||||
return current
|
||||
}
|
||||
|
||||
private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool {
|
||||
if let ch = ch {
|
||||
return "\n\r\t ,".contains(String(ch))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func readSegmentType() -> PathSegment.SegmentType? {
|
||||
while true {
|
||||
if let type = getPathSegmentType() {
|
||||
readNext()
|
||||
return type
|
||||
}
|
||||
if readNext() == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getPathSegmentType() -> PathSegment.SegmentType? {
|
||||
if let ch = current {
|
||||
switch ch {
|
||||
case "M":
|
||||
return .M
|
||||
case "m":
|
||||
return .m
|
||||
case "L":
|
||||
return .L
|
||||
case "l":
|
||||
return .l
|
||||
case "C":
|
||||
return .C
|
||||
case "c":
|
||||
return .c
|
||||
case "Q":
|
||||
return .Q
|
||||
case "q":
|
||||
return .q
|
||||
case "A":
|
||||
return .A
|
||||
case "a":
|
||||
return .a
|
||||
case "z", "Z":
|
||||
return .z
|
||||
case "H":
|
||||
return .H
|
||||
case "h":
|
||||
return .h
|
||||
case "V":
|
||||
return .V
|
||||
case "v":
|
||||
return .v
|
||||
case "S":
|
||||
return .S
|
||||
case "s":
|
||||
return .s
|
||||
case "T":
|
||||
return .T
|
||||
case "t":
|
||||
return .t
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int {
|
||||
switch segment {
|
||||
case .H, .h, .V, .v:
|
||||
return 1
|
||||
case .M, .m, .L, .l, .T, .t:
|
||||
return 2
|
||||
case .S, .s, .Q, .q:
|
||||
return 4
|
||||
case .C, .c:
|
||||
return 6
|
||||
case .A, .a:
|
||||
return 7
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","]
|
||||
private func decodeStickerThumbnailData(_ data: Data) -> String {
|
||||
var string = "M"
|
||||
data.forEach { byte in
|
||||
if byte >= 128 + 64 {
|
||||
string.append(decodingMap[Int(byte) - 128 - 64])
|
||||
} else {
|
||||
if byte >= 128 {
|
||||
string.append(",")
|
||||
} else if byte >= 64 {
|
||||
string.append("-")
|
||||
}
|
||||
string.append("\(byte & 63)")
|
||||
}
|
||||
}
|
||||
string.append("z")
|
||||
return string
|
||||
}
|
||||
|
||||
public func generateStickerPlaceholderImage(data: Data?, size: CGSize, scale: CGFloat? = nil, imageSize: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(size, scale: scale, rotatedContext: { size, context in
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
} else {
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
var path = decodeStickerThumbnailData(data)
|
||||
if !path.hasSuffix("z") {
|
||||
path = "\(path)z"
|
||||
}
|
||||
let reader = PathDataReader(input: path)
|
||||
let segments = reader.read()
|
||||
|
||||
let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height)
|
||||
context.scaleBy(x: scale, y: scale)
|
||||
renderPath(segments, context: context)
|
||||
} else {
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0))
|
||||
UIGraphicsPushContext(context)
|
||||
path.fill()
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
})
|
||||
}
|
21
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "shadow@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridShadow.imageset/shadow@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
12
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "smalleye.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
89
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf
vendored
Normal file
89
submodules/TelegramUI/Images.xcassets/Peer Info/MediaGridViewCount.imageset/smalleye.pdf
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.560059 0.300293 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
6.439604 8.399902 m
|
||||
3.031456 8.399902 0.941910 5.970886 0.174076 4.877690 c
|
||||
-0.050043 4.558603 -0.057914 4.146943 0.151409 3.817960 c
|
||||
0.892566 2.653118 2.962386 -0.000098 6.439604 -0.000098 c
|
||||
9.916823 -0.000098 11.986644 2.653119 12.727800 3.817960 c
|
||||
12.937123 4.146944 12.929253 4.558603 12.705133 4.877690 c
|
||||
11.937300 5.970886 9.847753 8.399902 6.439604 8.399902 c
|
||||
h
|
||||
9.439605 4.199902 m
|
||||
9.439605 2.543048 8.096458 1.199902 6.439604 1.199902 c
|
||||
4.782750 1.199902 3.439604 2.543048 3.439604 4.199902 c
|
||||
3.439604 5.856756 4.782750 7.199903 6.439604 7.199903 c
|
||||
8.096458 7.199903 9.439605 5.856756 9.439605 4.199902 c
|
||||
h
|
||||
6.439604 2.399902 m
|
||||
7.433717 2.399902 8.239604 3.205790 8.239604 4.199902 c
|
||||
8.239604 5.194015 7.433717 5.999902 6.439604 5.999902 c
|
||||
6.386777 5.999902 6.334482 5.997626 6.282810 5.993168 c
|
||||
6.382505 5.818204 6.439462 5.615724 6.439462 5.399942 c
|
||||
6.439462 4.737201 5.902204 4.199943 5.239462 4.199943 c
|
||||
5.023717 4.199943 4.821270 4.256880 4.646326 4.356544 c
|
||||
4.641876 4.304922 4.639604 4.252677 4.639604 4.199902 c
|
||||
4.639604 3.205790 5.445492 2.399902 6.439604 2.399902 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1211
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 14.000000 9.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000001301 00000 n
|
||||
0000001324 00000 n
|
||||
0000001496 00000 n
|
||||
0000001570 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1629
|
||||
%%EOF
|
@ -105,7 +105,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
let chatListFilters = chatSelection.chatListFilters
|
||||
|
||||
placeholder = placeholderValue
|
||||
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true)
|
||||
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout, displayPresence: chatSelection.displayPresence), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false, autoSetReady: true, isMainTab: false)
|
||||
chatListNode.passthroughPeerSelection = true
|
||||
chatListNode.disabledPeerSelected = { peer, _ in
|
||||
attemptDisabledItemSelection?(peer)
|
||||
|
Loading…
x
Reference in New Issue
Block a user