mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Media groups menu
This commit is contained in:
parent
6c5c9fd42e
commit
8e4da0cd18
@ -9796,3 +9796,5 @@ Sorry for the inconvenience.";
|
||||
"Premium.MaxStoriesMonthlyText" = "You can post **%@** stories in a month. Upgrade to **Telegram Premium** to increase this limit to **%@**.";
|
||||
"Premium.MaxStoriesMonthlyNoPremiumText" = "You have reached the limit of **%@** stories per month.";
|
||||
"Premium.MaxStoriesMonthlyFinalText" = "You have reached the limit of **%@** stories per month.";
|
||||
|
||||
"MediaPicker.Recents" = "Recents";
|
||||
|
@ -131,7 +131,11 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
if action.iconSource == nil {
|
||||
if let iconSource = action.iconSource {
|
||||
self.iconNode.clipsToBounds = true
|
||||
self.iconNode.contentMode = iconSource.contentMode
|
||||
self.iconNode.cornerRadius = iconSource.cornerRadius
|
||||
} else {
|
||||
self.iconNode.image = action.icon(presentationData.theme)
|
||||
}
|
||||
|
||||
|
@ -60,10 +60,14 @@ public enum ContextMenuActionItemFont {
|
||||
|
||||
public struct ContextMenuActionItemIconSource {
|
||||
public let size: CGSize
|
||||
public let contentMode: UIView.ContentMode
|
||||
public let cornerRadius: CGFloat
|
||||
public let signal: Signal<UIImage?, NoError>
|
||||
|
||||
public init(size: CGSize, signal: Signal<UIImage?, NoError>) {
|
||||
public init(size: CGSize, contentMode: UIView.ContentMode = .scaleToFill, cornerRadius: CGFloat = 0.0, signal: Signal<UIImage?, NoError>) {
|
||||
self.size = size
|
||||
self.contentMode = contentMode
|
||||
self.cornerRadius = cornerRadius
|
||||
self.signal = signal
|
||||
}
|
||||
}
|
||||
|
@ -287,6 +287,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
|
||||
let iconSize: CGSize?
|
||||
if let iconSource = self.item.iconSource {
|
||||
iconSize = iconSource.size
|
||||
self.iconNode.cornerRadius = iconSource.cornerRadius
|
||||
self.iconNode.contentMode = iconSource.contentMode
|
||||
self.iconNode.clipsToBounds = true
|
||||
if self.iconDisposable == nil {
|
||||
self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -0,0 +1,438 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ContextUI
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import Photos
|
||||
|
||||
struct MediaGroupItem {
|
||||
let collection: PHAssetCollection
|
||||
let firstItem: PHAsset?
|
||||
let count: Int
|
||||
}
|
||||
|
||||
final class MediaGroupsContextMenuContent: ContextControllerItemsContent {
|
||||
private final class GroupsListNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private final class ItemNode: HighlightTrackingButtonNode {
|
||||
let context: AccountContext
|
||||
let highlightBackgroundNode: ASDisplayNode
|
||||
let titleLabelNode: ImmediateTextNode
|
||||
let subtitleLabelNode: ImmediateTextNode
|
||||
let iconNode: ImageNode
|
||||
let separatorNode: ASDisplayNode
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
private var item: MediaGroupItem?
|
||||
|
||||
init(context: AccountContext, action: @escaping () -> Void) {
|
||||
self.action = action
|
||||
self.context = context
|
||||
|
||||
self.highlightBackgroundNode = ASDisplayNode()
|
||||
self.highlightBackgroundNode.isAccessibilityElement = false
|
||||
self.highlightBackgroundNode.alpha = 0.0
|
||||
|
||||
self.titleLabelNode = ImmediateTextNode()
|
||||
self.titleLabelNode.isAccessibilityElement = false
|
||||
self.titleLabelNode.maximumNumberOfLines = 1
|
||||
self.titleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.subtitleLabelNode = ImmediateTextNode()
|
||||
self.subtitleLabelNode.isAccessibilityElement = false
|
||||
self.subtitleLabelNode.maximumNumberOfLines = 1
|
||||
self.subtitleLabelNode.isUserInteractionEnabled = false
|
||||
|
||||
self.iconNode = ImageNode()
|
||||
self.iconNode.clipsToBounds = true
|
||||
self.iconNode.contentMode = .scaleAspectFill
|
||||
self.iconNode.cornerRadius = 6.0
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isAccessibilityElement = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.highlightBackgroundNode)
|
||||
self.addSubnode(self.titleLabelNode)
|
||||
self.addSubnode(self.subtitleLabelNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
strongSelf.highlightBackgroundNode.alpha = 1.0
|
||||
} else {
|
||||
let previousAlpha = strongSelf.highlightBackgroundNode.alpha
|
||||
strongSelf.highlightBackgroundNode.alpha = 0.0
|
||||
strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.action()
|
||||
}
|
||||
|
||||
func update(size: CGSize, presentationData: PresentationData, item: MediaGroupItem, isLast: Bool, syncronousLoad: Bool) {
|
||||
let leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 48.0
|
||||
|
||||
if self.item?.collection.localIdentifier != item.collection.localIdentifier {
|
||||
self.item = item
|
||||
|
||||
self.accessibilityLabel = item.collection.localizedTitle
|
||||
|
||||
if let asset = item.firstItem {
|
||||
self.iconNode.setSignal(assetImage(asset: asset, targetSize: CGSize(width: 24.0, height: 24.0), exact: false))
|
||||
}
|
||||
}
|
||||
|
||||
self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
|
||||
self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.titleLabelNode.attributedText = NSAttributedString(string: item.collection.localizedTitle ?? "", font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor)
|
||||
|
||||
self.subtitleLabelNode.attributedText = NSAttributedString(string: "\(item.count)", font: Font.regular(15.0), textColor: presentationData.theme.contextMenu.secondaryColor)
|
||||
let maxTextWidth: CGFloat = size.width - leftInset - rightInset
|
||||
|
||||
let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
|
||||
let subtitleSize = self.subtitleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0))
|
||||
|
||||
let spacing: CGFloat = 2.0
|
||||
let contentHeight = titleSize.height + spacing + subtitleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - contentHeight) / 2.0)), size: titleSize)
|
||||
self.titleLabelNode.frame = titleFrame
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + spacing), size: titleSize)
|
||||
self.subtitleLabelNode.frame = subtitleFrame
|
||||
|
||||
let iconSize = CGSize(width: 24.0, height: 24.0)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: size.width - leftInset - iconSize.width, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
self.iconNode.frame = iconFrame
|
||||
|
||||
self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
self.separatorNode.isHidden = isLast
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let items: [MediaGroupItem]
|
||||
private let requestUpdate: (GroupsListNode, ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (GroupsListNode, ContainedViewLayoutTransition) -> Void
|
||||
private let selectGroup: (PHAssetCollection) -> Void
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var animateIn: Bool = false
|
||||
private var bottomScrollInset: CGFloat = 0.0
|
||||
|
||||
private var presentationData: PresentationData?
|
||||
private var currentSize: CGSize?
|
||||
private var apparentHeight: CGFloat = 0.0
|
||||
|
||||
private var itemNodes: [Int: ItemNode] = [:]
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: [MediaGroupItem],
|
||||
requestUpdate: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void,
|
||||
selectGroup: @escaping (PHAssetCollection) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
self.selectGroup = selectGroup
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollNode.clipsToBounds = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
self.scrollNode.view.delegate = self
|
||||
|
||||
self.clipsToBounds = true
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if self.ignoreScrolling {
|
||||
return
|
||||
}
|
||||
self.updateVisibleItems(animated: false, syncronousLoad: false)
|
||||
|
||||
if let size = self.currentSize {
|
||||
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
||||
apparentHeight = max(apparentHeight, 44.0)
|
||||
apparentHeight = min(apparentHeight, size.height)
|
||||
if self.apparentHeight != apparentHeight {
|
||||
self.apparentHeight = apparentHeight
|
||||
|
||||
self.requestUpdateApparentHeight(self, .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) {
|
||||
guard let size = self.currentSize else {
|
||||
return
|
||||
}
|
||||
guard let presentationData = self.presentationData else {
|
||||
return
|
||||
}
|
||||
let itemHeight: CGFloat = 54.0
|
||||
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0)
|
||||
|
||||
var validIds = Set<Int>()
|
||||
|
||||
let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight)))
|
||||
let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight))
|
||||
|
||||
if minVisibleIndex <= maxVisibleIndex {
|
||||
for index in minVisibleIndex ... maxVisibleIndex {
|
||||
if index < self.items.count {
|
||||
let height = itemHeight
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: height))
|
||||
|
||||
let item = self.items[index]
|
||||
validIds.insert(index)
|
||||
|
||||
let itemNode: ItemNode
|
||||
if let current = self.itemNodes[index] {
|
||||
itemNode = current
|
||||
} else {
|
||||
let selectGroup = self.selectGroup
|
||||
itemNode = ItemNode(context: self.context, action: {
|
||||
selectGroup(item.collection)
|
||||
})
|
||||
self.itemNodes[index] = itemNode
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
|
||||
itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: index == self.items.count - 1, syncronousLoad: syncronousLoad)
|
||||
itemNode.frame = itemFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var removeIds: [Int] = []
|
||||
for (id, itemNode) in self.itemNodes {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
itemNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.itemNodes.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
var extendedScrollNodeFrame = self.scrollNode.frame
|
||||
extendedScrollNodeFrame.size.height += self.bottomScrollInset
|
||||
|
||||
if extendedScrollNodeFrame.contains(point) {
|
||||
return self.scrollNode.view.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event)
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedSize: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) {
|
||||
let itemHeight: CGFloat = 54.0
|
||||
|
||||
self.presentationData = presentationData
|
||||
|
||||
let contentHeight = CGFloat(self.items.count) * itemHeight
|
||||
let size = CGSize(width: constrainedSize.width, height: contentHeight)
|
||||
|
||||
let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height))
|
||||
self.currentSize = containerSize
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) {
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
if self.scrollNode.view.contentInset.bottom != bottomInset {
|
||||
self.scrollNode.view.contentInset.bottom = bottomInset
|
||||
}
|
||||
self.bottomScrollInset = bottomInset
|
||||
let scrollContentSize = CGSize(width: size.width, height: size.height)
|
||||
if self.scrollNode.view.contentSize != scrollContentSize {
|
||||
self.scrollNode.view.contentSize = scrollContentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated)
|
||||
|
||||
self.animateIn = false
|
||||
|
||||
var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height
|
||||
apparentHeight = max(apparentHeight, 44.0)
|
||||
apparentHeight = min(apparentHeight, containerSize.height)
|
||||
self.apparentHeight = apparentHeight
|
||||
|
||||
return (containerSize.height, apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemsNode: ASDisplayNode, ContextControllerItemsNode {
|
||||
private let context: AccountContext
|
||||
private let items: [MediaGroupItem]
|
||||
private let requestUpdate: (ContainedViewLayoutTransition) -> Void
|
||||
private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let currentTabIndex: Int = 0
|
||||
private var visibleTabNodes: [Int: GroupsListNode] = [:]
|
||||
|
||||
private let selectGroup: (PHAssetCollection) -> Void
|
||||
|
||||
private(set) var apparentHeight: CGFloat = 0.0
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: [MediaGroupItem],
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
selectGroup: @escaping (PHAssetCollection) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectGroup = selectGroup
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
self.requestUpdate = requestUpdate
|
||||
self.requestUpdateApparentHeight = requestUpdateApparentHeight
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) {
|
||||
let constrainedSize = CGSize(width: min(190.0, constrainedWidth), height: min(295.0, maxHeight))
|
||||
|
||||
let topContentHeight: CGFloat = 0.0
|
||||
|
||||
var tabLayouts: [Int: (height: CGFloat, apparentHeight: CGFloat)] = [:]
|
||||
|
||||
var visibleIndices: [Int] = []
|
||||
visibleIndices.append(self.currentTabIndex)
|
||||
|
||||
let previousVisibleTabFrames: [(Int, CGRect)] = self.visibleTabNodes.map { key, value -> (Int, CGRect) in
|
||||
return (key, value.frame)
|
||||
}
|
||||
|
||||
for index in visibleIndices {
|
||||
var tabTransition = transition
|
||||
let tabNode: GroupsListNode
|
||||
var initialReferenceFrame: CGRect?
|
||||
if let current = self.visibleTabNodes[index] {
|
||||
tabNode = current
|
||||
} else {
|
||||
for (previousIndex, previousFrame) in previousVisibleTabFrames {
|
||||
if index > previousIndex {
|
||||
initialReferenceFrame = previousFrame.offsetBy(dx: constrainedSize.width, dy: 0.0)
|
||||
} else {
|
||||
initialReferenceFrame = previousFrame.offsetBy(dx: -constrainedSize.width, dy: 0.0)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
tabNode = GroupsListNode(
|
||||
context: self.context,
|
||||
items: self.items,
|
||||
requestUpdate: { [weak self] tab, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
|
||||
strongSelf.requestUpdate(transition)
|
||||
}
|
||||
},
|
||||
requestUpdateApparentHeight: { [weak self] tab, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) {
|
||||
strongSelf.requestUpdateApparentHeight(transition)
|
||||
}
|
||||
},
|
||||
selectGroup: self.selectGroup
|
||||
)
|
||||
self.addSubnode(tabNode)
|
||||
self.visibleTabNodes[index] = tabNode
|
||||
tabTransition = .immediate
|
||||
}
|
||||
|
||||
let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), bottomInset: bottomInset, transition: tabTransition)
|
||||
tabLayouts[index] = tabLayout
|
||||
let currentFractionalTabIndex = CGFloat(self.currentTabIndex)
|
||||
let xOffset: CGFloat = (CGFloat(index) - currentFractionalTabIndex) * constrainedSize.width
|
||||
let tabFrame = CGRect(origin: CGPoint(x: xOffset, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: tabLayout.height))
|
||||
tabTransition.updateFrame(node: tabNode, frame: tabFrame)
|
||||
if let initialReferenceFrame = initialReferenceFrame {
|
||||
transition.animatePositionAdditive(node: tabNode, offset: CGPoint(x: initialReferenceFrame.minX - tabFrame.minX, y: 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
var contentSize = CGSize(width: constrainedSize.width, height: topContentHeight)
|
||||
var apparentHeight = topContentHeight
|
||||
|
||||
if let tabLayout = tabLayouts[self.currentTabIndex] {
|
||||
contentSize.height += tabLayout.height
|
||||
apparentHeight += tabLayout.apparentHeight
|
||||
}
|
||||
|
||||
return (contentSize, apparentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let items: [MediaGroupItem]
|
||||
let selectGroup: (PHAssetCollection) -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
items: [MediaGroupItem],
|
||||
selectGroup: @escaping (PHAssetCollection) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectGroup = selectGroup
|
||||
}
|
||||
|
||||
func node(
|
||||
requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void,
|
||||
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
|
||||
) -> ContextControllerItemsNode {
|
||||
return ItemsNode(
|
||||
context: self.context,
|
||||
items: self.items,
|
||||
requestUpdate: requestUpdate,
|
||||
requestUpdateApparentHeight: requestUpdateApparentHeight,
|
||||
selectGroup: self.selectGroup
|
||||
)
|
||||
}
|
||||
}
|
@ -186,6 +186,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
public var isContainerPanning: () -> Bool = { return false }
|
||||
public var isContainerExpanded: () -> Bool = { return false }
|
||||
|
||||
private let selectedCollection = Promise<PHAssetCollection?>(nil)
|
||||
|
||||
var dismissAll: () -> Void = { }
|
||||
|
||||
private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||
@ -237,7 +239,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
private var fastScrollContentOffset = ValuePromise<CGPoint>(ignoreRepeated: true)
|
||||
private var fastScrollDisposable: Disposable?
|
||||
|
||||
|
||||
private var didSetReady = false
|
||||
private let _ready = Promise<Bool>()
|
||||
var ready: Promise<Bool> {
|
||||
@ -284,6 +286,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.containerNode.addSubnode(self.gridNode)
|
||||
self.containerNode.addSubnode(self.scrollingArea)
|
||||
|
||||
let selectedCollection = controller.selectedCollection.get()
|
||||
let preloadPromise = self.preloadPromise
|
||||
let updatedState: Signal<State, NoError>
|
||||
switch controller.subject {
|
||||
@ -301,15 +304,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
} else if [.restricted, .denied].contains(mediaAccess) {
|
||||
return .single(.noAccess(cameraAccess: cameraAccess))
|
||||
} else {
|
||||
if let collection = collection {
|
||||
return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get())
|
||||
|> map { fetchResult, preload in
|
||||
return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: cameraAccess)
|
||||
}
|
||||
} else {
|
||||
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts)
|
||||
|> map { fetchResult, preload, drafts in
|
||||
return .assets(fetchResult: fetchResult, preload: preload, drafts: drafts, mediaAccess: mediaAccess, cameraAccess: cameraAccess)
|
||||
return selectedCollection
|
||||
|> mapToSignal { selectedCollection in
|
||||
let collection = selectedCollection ?? collection
|
||||
if let collection {
|
||||
return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get())
|
||||
|> map { fetchResult, preload in
|
||||
return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: selectedCollection != nil ? nil : cameraAccess)
|
||||
}
|
||||
} else {
|
||||
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts)
|
||||
|> map { fetchResult, preload, drafts in
|
||||
return .assets(fetchResult: fetchResult, preload: preload, drafts: drafts, mediaAccess: mediaAccess, cameraAccess: cameraAccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1455,8 +1462,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
|
||||
} else {
|
||||
switch mode {
|
||||
case .default, .story:
|
||||
self.titleView.title = presentationData.strings.Attachment_Gallery
|
||||
case .default:
|
||||
self.titleView.title = presentationData.strings.MediaPicker_Recents
|
||||
self.titleView.isEnabled = true
|
||||
case .story:
|
||||
self.titleView.title = presentationData.strings.MediaPicker_Recents
|
||||
self.titleView.isEnabled = true
|
||||
case .wallpaper:
|
||||
self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle
|
||||
case .addImage:
|
||||
@ -1527,6 +1538,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
strongSelf.controllerNode.updateDisplayMode(index == 0 ? .all : .selected)
|
||||
}
|
||||
}
|
||||
|
||||
self.titleView.action = { [weak self] in
|
||||
if let self {
|
||||
self.openGroupsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
||||
@ -1703,6 +1720,114 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.controllerNode.closeGalleryController()
|
||||
}
|
||||
|
||||
public func openGroupsMenu() {
|
||||
let updatedState = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.controllerNode.mediaAssetsContext.fetchAssetsCollections(.album),
|
||||
self.controllerNode.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
|
||||
)
|
||||
let _ = (updatedState
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] albums, smartAlbums in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var collections: [PHAssetCollection] = []
|
||||
smartAlbums.enumerateObjects { collection, _, _ in
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
smartAlbums.enumerateObjects { collection, index, _ in
|
||||
var supportedAlbums: [PHAssetCollectionSubtype] = [
|
||||
.smartAlbumBursts,
|
||||
.smartAlbumPanoramas,
|
||||
.smartAlbumScreenshots,
|
||||
.smartAlbumSelfPortraits,
|
||||
.smartAlbumSlomoVideos,
|
||||
.smartAlbumTimelapses,
|
||||
.smartAlbumVideos,
|
||||
.smartAlbumAllHidden
|
||||
]
|
||||
if #available(iOS 11, *) {
|
||||
supportedAlbums.append(.smartAlbumAnimated)
|
||||
supportedAlbums.append(.smartAlbumDepthEffect)
|
||||
supportedAlbums.append(.smartAlbumLivePhotos)
|
||||
}
|
||||
if supportedAlbums.contains(collection.assetCollectionSubtype) {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
if result.count > 0 {
|
||||
collections.append(collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
|
||||
collections.append(collection)
|
||||
}
|
||||
|
||||
var items: [MediaGroupItem] = []
|
||||
for collection in collections {
|
||||
let result = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
let firstItem: PHAsset?
|
||||
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
|
||||
firstItem = result.lastObject
|
||||
} else {
|
||||
firstItem = result.firstObject
|
||||
}
|
||||
// let iconSource: ContextMenuActionItemIconSource?
|
||||
// if let firstItem {
|
||||
// let targetSize = CGSize(width: 24.0, height: 24.0)
|
||||
// iconSource = ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), contentMode: .scaleAspectFill, cornerRadius: 6.0, signal: assetImage(asset: firstItem, targetSize: targetSize, exact: false))
|
||||
// } else {
|
||||
// iconSource = nil
|
||||
// }
|
||||
items.append(
|
||||
MediaGroupItem(
|
||||
collection: collection,
|
||||
firstItem: firstItem,
|
||||
count: result.count
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var dismissImpl: (() -> Void)?
|
||||
let content: ContextControllerItemsContent = MediaGroupsContextMenuContent(
|
||||
context: self.context,
|
||||
items: items,
|
||||
selectGroup: { [weak self] collection in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
|
||||
self.selectedCollection.set(.single(nil))
|
||||
self.titleView.title = self.presentationData.strings.MediaPicker_Recents
|
||||
} else {
|
||||
self.selectedCollection.set(.single(collection))
|
||||
self.titleView.title = collection.localizedTitle ?? ""
|
||||
}
|
||||
dismissImpl?()
|
||||
}
|
||||
)
|
||||
|
||||
self.titleView.isHighlighted = true
|
||||
let contextController = ContextController(
|
||||
account: self.context.account,
|
||||
presentationData: self.presentationData,
|
||||
source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: self.titleView.contextSourceNode)),
|
||||
items: .single(ContextController.Items(content: .custom(content))),
|
||||
gesture: nil
|
||||
)
|
||||
contextController.dismissed = { [weak self] in
|
||||
self?.titleView.isHighlighted = false
|
||||
}
|
||||
dismissImpl = { [weak contextController] in
|
||||
contextController?.dismiss()
|
||||
}
|
||||
self.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
}
|
||||
|
||||
private weak var undoOverlayController: UndoOverlayController?
|
||||
private func showSelectionUndo(item: TGMediaSelectableItem) {
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
|
@ -4,9 +4,13 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import SegmentedControlNode
|
||||
import ContextUI
|
||||
|
||||
final class MediaPickerTitleView: UIView {
|
||||
let contextSourceNode: ContextReferenceContentNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let arrowNode: ASImageNode
|
||||
private let segmentedControlNode: SegmentedControlNode
|
||||
|
||||
public var theme: PresentationTheme {
|
||||
@ -25,6 +29,19 @@ final class MediaPickerTitleView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public var isEnabled: Bool = false {
|
||||
didSet {
|
||||
self.buttonNode.isUserInteractionEnabled = self.isEnabled
|
||||
self.arrowNode.isHidden = !self.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
public var isHighlighted: Bool = false {
|
||||
didSet {
|
||||
self.alpha = self.isHighlighted ? 0.5 : 1.0
|
||||
}
|
||||
}
|
||||
|
||||
public var segmentsHidden = true {
|
||||
didSet {
|
||||
if self.segmentsHidden != oldValue {
|
||||
@ -55,14 +72,23 @@ final class MediaPickerTitleView: UIView {
|
||||
}
|
||||
|
||||
public var indexUpdated: ((Int) -> Void)?
|
||||
public var action: () -> Void = {}
|
||||
|
||||
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
|
||||
self.theme = theme
|
||||
self.segments = segments
|
||||
|
||||
self.contextSourceNode = ContextReferenceContentNode()
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.displaysAsynchronously = false
|
||||
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Stories/SelectorArrowDown"), color: theme.rootController.navigationBar.secondaryTextColor)
|
||||
self.arrowNode.isHidden = true
|
||||
|
||||
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex)
|
||||
self.segmentedControlNode.alpha = 0.0
|
||||
self.segmentedControlNode.isUserInteractionEnabled = false
|
||||
@ -73,8 +99,26 @@ final class MediaPickerTitleView: UIView {
|
||||
self?.indexUpdated?(index)
|
||||
}
|
||||
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.arrowNode.alpha = 0.5
|
||||
self.titleNode.alpha = 0.5
|
||||
} else {
|
||||
self.arrowNode.alpha = 1.0
|
||||
self.titleNode.alpha = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubnode(self.contextSourceNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.arrowNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.segmentedControlNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
@ -85,10 +129,21 @@ final class MediaPickerTitleView: UIView {
|
||||
super.layoutSubviews()
|
||||
|
||||
let size = self.bounds.size
|
||||
self.contextSourceNode.frame = self.bounds.insetBy(dx: 0.0, dy: 14.0)
|
||||
|
||||
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: min(300.0, size.width - 36.0)), transition: .immediate)
|
||||
self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
|
||||
if let arrowSize = self.arrowNode.image?.size {
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0) + 1.0 - UIScreenPixel), size: arrowSize)
|
||||
}
|
||||
self.buttonNode.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.action()
|
||||
}
|
||||
}
|
||||
|
@ -570,7 +570,10 @@ public final class MediaEditor {
|
||||
} else if case .forceRendering = mode {
|
||||
self.forceRendering = true
|
||||
}
|
||||
self.values = f(self.values)
|
||||
let updatedValues = f(self.values)
|
||||
if self.values != updatedValues {
|
||||
self.values = updatedValues
|
||||
}
|
||||
if case .skipRendering = mode {
|
||||
self.skipRendering = false
|
||||
} else if case .forceRendering = mode {
|
||||
|
Loading…
x
Reference in New Issue
Block a user