mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Attachment menu improvements
This commit is contained in:
parent
33f503c41e
commit
ac543cb093
@ -98,7 +98,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
return 210.0
|
return 210.0
|
||||||
}
|
}
|
||||||
if case .compact = layout.metrics.widthClass {
|
if case .compact = layout.metrics.widthClass {
|
||||||
return min(layout.size.width, layout.size.height) * 0.2488
|
return max(layout.size.width, layout.size.height) * 0.2488
|
||||||
} else {
|
} else {
|
||||||
return 210.0
|
return 210.0
|
||||||
}
|
}
|
||||||
|
101
submodules/MediaPickerUI/Sources/MediaAssetsContext.swift
Normal file
101
submodules/MediaPickerUI/Sources/MediaAssetsContext.swift
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import Photos
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
|
||||||
|
private var registeredChangeObserver = false
|
||||||
|
private let changeSink = ValuePipe<PHChange>()
|
||||||
|
private let mediaAccessSink = ValuePipe<PHAuthorizationStatus>()
|
||||||
|
private let cameraAccessSink = ValuePipe<AVAuthorizationStatus?>()
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
if PHPhotoLibrary.authorizationStatus() == .authorized {
|
||||||
|
PHPhotoLibrary.shared().register(self)
|
||||||
|
self.registeredChangeObserver = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if self.registeredChangeObserver {
|
||||||
|
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||||
|
self.changeSink.putNext(changeInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchResultAssets(_ initialFetchResult: PHFetchResult<PHAsset>) -> Signal<PHFetchResult<PHAsset>?, NoError> {
|
||||||
|
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
|
||||||
|
return .single(initialFetchResult)
|
||||||
|
|> then(
|
||||||
|
self.changeSink.signal()
|
||||||
|
|> mapToSignal { change in
|
||||||
|
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
|
||||||
|
let _ = fetchResult.modify { _ in return updatedFetchResult }
|
||||||
|
return .single(updatedFetchResult)
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recentAssets() -> Signal<PHFetchResult<PHAsset>?, NoError> {
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
|
||||||
|
if let collection = collections.firstObject {
|
||||||
|
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
|
||||||
|
return fetchResultAssets(initialFetchResult)
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaAccess() -> Signal<PHAuthorizationStatus, NoError> {
|
||||||
|
let initialStatus: PHAuthorizationStatus
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
initialStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||||||
|
} else {
|
||||||
|
initialStatus = PHPhotoLibrary.authorizationStatus()
|
||||||
|
}
|
||||||
|
return .single(initialStatus)
|
||||||
|
|> then(
|
||||||
|
self.mediaAccessSink.signal()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestMediaAccess() -> Void {
|
||||||
|
PHPhotoLibrary.requestAuthorization { [weak self] status in
|
||||||
|
self?.mediaAccessSink.putNext(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cameraAccess() -> Signal<AVAuthorizationStatus?, NoError> {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
return .single(.authorized)
|
||||||
|
#else
|
||||||
|
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
||||||
|
return .single(AVCaptureDevice.authorizationStatus(for: .video))
|
||||||
|
|> then(
|
||||||
|
self.cameraAccessSink.signal()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestCameraAccess() -> Void {
|
||||||
|
AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] result in
|
||||||
|
if result {
|
||||||
|
self?.cameraAccessSink.putNext(.authorized)
|
||||||
|
} else {
|
||||||
|
self?.cameraAccessSink.putNext(.denied)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
8
submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift
Normal file
8
submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// MediaGroupsScreen.swift
|
||||||
|
// _idx_TelegramUI_F082088E_ios_min9.0
|
||||||
|
//
|
||||||
|
// Created by Ilya Laktyushin on 22.02.2022.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
131
submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift
Normal file
131
submodules/MediaPickerUI/Sources/MediaPickerMoreButton.swift
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ManagedAnimationNode
|
||||||
|
import ContextUI
|
||||||
|
|
||||||
|
final class MediaPickerMoreButtonNode: ASDisplayNode {
|
||||||
|
class MoreIconNode: ManagedAnimationNode {
|
||||||
|
enum State: Equatable {
|
||||||
|
case more
|
||||||
|
case search
|
||||||
|
}
|
||||||
|
|
||||||
|
private let duration: Double = 0.21
|
||||||
|
var iconState: State = .search
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(size: CGSize(width: 30.0, height: 30.0))
|
||||||
|
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func play() {
|
||||||
|
if case .more = self.iconState {
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueState(_ state: State, animated: Bool) {
|
||||||
|
guard self.iconState != state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousState = self.iconState
|
||||||
|
self.iconState = state
|
||||||
|
|
||||||
|
let source = ManagedAnimationSource.local("anim_moretosearch")
|
||||||
|
|
||||||
|
let totalLength: Int = 90
|
||||||
|
if animated {
|
||||||
|
switch previousState {
|
||||||
|
case .more:
|
||||||
|
switch state {
|
||||||
|
case .more:
|
||||||
|
break
|
||||||
|
case .search:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
|
||||||
|
}
|
||||||
|
case .search:
|
||||||
|
switch state {
|
||||||
|
case .more:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
|
||||||
|
case .search:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch state {
|
||||||
|
case .more:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
|
||||||
|
case .search:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var action: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||||
|
|
||||||
|
private let containerNode: ContextControllerSourceNode
|
||||||
|
let contextSourceNode: ContextReferenceContentNode
|
||||||
|
private let buttonNode: HighlightableButtonNode
|
||||||
|
let iconNode: MoreIconNode
|
||||||
|
|
||||||
|
var theme: PresentationTheme {
|
||||||
|
didSet {
|
||||||
|
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(theme: PresentationTheme) {
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
self.contextSourceNode = ContextReferenceContentNode()
|
||||||
|
self.containerNode = ContextControllerSourceNode()
|
||||||
|
self.containerNode.animateScale = false
|
||||||
|
|
||||||
|
self.buttonNode = HighlightableButtonNode()
|
||||||
|
self.iconNode = MoreIconNode()
|
||||||
|
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.buttonNode)
|
||||||
|
|
||||||
|
self.buttonNode.addSubnode(self.containerNode)
|
||||||
|
self.containerNode.addSubnode(self.contextSourceNode)
|
||||||
|
self.contextSourceNode.addSubnode(self.iconNode)
|
||||||
|
|
||||||
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
|
|
||||||
|
self.containerNode.activated = { [weak self] gesture, _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.action?(strongSelf.contextSourceNode, gesture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func buttonPressed() {
|
||||||
|
self.action?(self.contextSourceNode, nil)
|
||||||
|
if case .more = self.iconNode.iconState {
|
||||||
|
self.iconNode.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||||
|
let animationSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
let inset: CGFloat = 0.0
|
||||||
|
self.iconNode.frame = CGRect(origin: CGPoint(x: inset + 4.0, y: floor((constrainedSize.height - animationSize.height) / 2.0)), size: animationSize)
|
||||||
|
|
||||||
|
let size = CGSize(width: animationSize.width + inset * 2.0, height: constrainedSize.height)
|
||||||
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
|
self.buttonNode.frame = bounds
|
||||||
|
self.containerNode.frame = bounds
|
||||||
|
self.contextSourceNode.frame = bounds
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
@ -12,109 +12,11 @@ import MergeLists
|
|||||||
import Photos
|
import Photos
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import LegacyComponents
|
import LegacyComponents
|
||||||
import AttachmentUI
|
|
||||||
import SegmentedControlNode
|
|
||||||
import ManagedAnimationNode
|
|
||||||
import ContextUI
|
|
||||||
import LegacyMediaPickerUI
|
import LegacyMediaPickerUI
|
||||||
|
import AttachmentUI
|
||||||
|
import ContextUI
|
||||||
import WebSearchUI
|
import WebSearchUI
|
||||||
|
|
||||||
private class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
|
|
||||||
private var registeredChangeObserver = false
|
|
||||||
private let changeSink = ValuePipe<PHChange>()
|
|
||||||
private let mediaAccessSink = ValuePipe<PHAuthorizationStatus>()
|
|
||||||
private let cameraAccessSink = ValuePipe<AVAuthorizationStatus?>()
|
|
||||||
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
if PHPhotoLibrary.authorizationStatus() == .authorized {
|
|
||||||
PHPhotoLibrary.shared().register(self)
|
|
||||||
self.registeredChangeObserver = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if self.registeredChangeObserver {
|
|
||||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
|
||||||
self.changeSink.putNext(changeInstance)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchResultAssets(_ initialFetchResult: PHFetchResult<PHAsset>) -> Signal<PHFetchResult<PHAsset>?, NoError> {
|
|
||||||
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
|
|
||||||
return .single(initialFetchResult)
|
|
||||||
|> then(
|
|
||||||
self.changeSink.signal()
|
|
||||||
|> mapToSignal { change in
|
|
||||||
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
|
|
||||||
let _ = fetchResult.modify { _ in return updatedFetchResult }
|
|
||||||
return .single(updatedFetchResult)
|
|
||||||
} else {
|
|
||||||
return .complete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func recentAssets() -> Signal<PHFetchResult<PHAsset>?, NoError> {
|
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
|
|
||||||
if let collection = collections.firstObject {
|
|
||||||
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
|
|
||||||
return fetchResultAssets(initialFetchResult)
|
|
||||||
} else {
|
|
||||||
return .single(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mediaAccess() -> Signal<PHAuthorizationStatus, NoError> {
|
|
||||||
let initialStatus: PHAuthorizationStatus
|
|
||||||
if #available(iOS 14.0, *) {
|
|
||||||
initialStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
||||||
} else {
|
|
||||||
initialStatus = PHPhotoLibrary.authorizationStatus()
|
|
||||||
}
|
|
||||||
return .single(initialStatus)
|
|
||||||
|> then(
|
|
||||||
self.mediaAccessSink.signal()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestMediaAccess() -> Void {
|
|
||||||
PHPhotoLibrary.requestAuthorization { [weak self] status in
|
|
||||||
self?.mediaAccessSink.putNext(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func cameraAccess() -> Signal<AVAuthorizationStatus?, NoError> {
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
return .single(.authorized)
|
|
||||||
#else
|
|
||||||
if UIImagePickerController.isSourceTypeAvailable(.camera) {
|
|
||||||
return .single(AVCaptureDevice.authorizationStatus(for: .video))
|
|
||||||
|> then(
|
|
||||||
self.cameraAccessSink.signal()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return .single(nil)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestCameraAccess() -> Void {
|
|
||||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] result in
|
|
||||||
if result {
|
|
||||||
self?.cameraAccessSink.putNext(.authorized)
|
|
||||||
} else {
|
|
||||||
self?.cameraAccessSink.putNext(.denied)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MediaPickerInteraction {
|
final class MediaPickerInteraction {
|
||||||
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
|
let openMedia: (PHFetchResult<PHAsset>, Int, UIImage?) -> Void
|
||||||
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
|
let openSelectedMedia: (TGMediaSelectableItem, UIImage?) -> Void
|
||||||
@ -166,218 +68,6 @@ private struct MediaPickerGridTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class MediaPickerSegmentedTitleView: UIView {
|
|
||||||
private let titleNode: ImmediateTextNode
|
|
||||||
private let segmentedControlNode: SegmentedControlNode
|
|
||||||
|
|
||||||
public var theme: PresentationTheme {
|
|
||||||
didSet {
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
|
|
||||||
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var title: String = "" {
|
|
||||||
didSet {
|
|
||||||
if self.title != oldValue {
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var segmentsHidden = true {
|
|
||||||
didSet {
|
|
||||||
if self.segmentsHidden != oldValue {
|
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut)
|
|
||||||
transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
|
|
||||||
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
|
|
||||||
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var segments: [String] {
|
|
||||||
didSet {
|
|
||||||
if self.segments != oldValue {
|
|
||||||
self.segmentedControlNode.items = self.segments.map { SegmentedControlItem(title: $0) }
|
|
||||||
self.setNeedsLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var index: Int {
|
|
||||||
get {
|
|
||||||
return self.segmentedControlNode.selectedIndex
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
self.segmentedControlNode.selectedIndex = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var indexUpdated: ((Int) -> Void)?
|
|
||||||
|
|
||||||
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
|
|
||||||
self.theme = theme
|
|
||||||
self.segments = segments
|
|
||||||
|
|
||||||
self.titleNode = ImmediateTextNode()
|
|
||||||
self.titleNode.displaysAsynchronously = false
|
|
||||||
|
|
||||||
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex)
|
|
||||||
self.segmentedControlNode.alpha = 0.0
|
|
||||||
self.segmentedControlNode.isUserInteractionEnabled = false
|
|
||||||
|
|
||||||
super.init(frame: CGRect())
|
|
||||||
|
|
||||||
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
|
|
||||||
self?.indexUpdated?(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addSubnode(self.titleNode)
|
|
||||||
self.addSubnode(self.segmentedControlNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
let size = self.bounds.size
|
|
||||||
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: 160.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class MediaPickerMoreButtonNode: ASDisplayNode {
|
|
||||||
fileprivate final class MoreIconNode: ManagedAnimationNode {
|
|
||||||
enum State: Equatable {
|
|
||||||
case more
|
|
||||||
case search
|
|
||||||
}
|
|
||||||
|
|
||||||
private let duration: Double = 0.21
|
|
||||||
var iconState: State = .search
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(size: CGSize(width: 30.0, height: 30.0))
|
|
||||||
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 90, endFrame: 90), duration: 0.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func play() {
|
|
||||||
if case .more = self.iconState {
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueState(_ state: State, animated: Bool) {
|
|
||||||
guard self.iconState != state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousState = self.iconState
|
|
||||||
self.iconState = state
|
|
||||||
|
|
||||||
let source = ManagedAnimationSource.local("anim_moretosearch")
|
|
||||||
|
|
||||||
let totalLength: Int = 90
|
|
||||||
if animated {
|
|
||||||
switch previousState {
|
|
||||||
case .more:
|
|
||||||
switch state {
|
|
||||||
case .more:
|
|
||||||
break
|
|
||||||
case .search:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
|
|
||||||
}
|
|
||||||
case .search:
|
|
||||||
switch state {
|
|
||||||
case .more:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
|
|
||||||
case .search:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch state {
|
|
||||||
case .more:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
|
|
||||||
case .search:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var action: ((ASDisplayNode, ContextGesture?) -> Void)?
|
|
||||||
|
|
||||||
private let containerNode: ContextControllerSourceNode
|
|
||||||
let contextSourceNode: ContextReferenceContentNode
|
|
||||||
private let buttonNode: HighlightableButtonNode
|
|
||||||
let iconNode: MoreIconNode
|
|
||||||
|
|
||||||
var theme: PresentationTheme {
|
|
||||||
didSet {
|
|
||||||
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(theme: PresentationTheme) {
|
|
||||||
self.theme = theme
|
|
||||||
|
|
||||||
self.contextSourceNode = ContextReferenceContentNode()
|
|
||||||
self.containerNode = ContextControllerSourceNode()
|
|
||||||
self.containerNode.animateScale = false
|
|
||||||
|
|
||||||
self.buttonNode = HighlightableButtonNode()
|
|
||||||
self.iconNode = MoreIconNode()
|
|
||||||
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.buttonNode)
|
|
||||||
|
|
||||||
self.buttonNode.addSubnode(self.containerNode)
|
|
||||||
self.containerNode.addSubnode(self.contextSourceNode)
|
|
||||||
self.contextSourceNode.addSubnode(self.iconNode)
|
|
||||||
|
|
||||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
||||||
|
|
||||||
self.containerNode.activated = { [weak self] gesture, _ in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.action?(strongSelf.contextSourceNode, gesture)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func buttonPressed() {
|
|
||||||
self.action?(self.contextSourceNode, nil)
|
|
||||||
if case .more = self.iconNode.iconState {
|
|
||||||
self.iconNode.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
|
||||||
let animationSize = CGSize(width: 30.0, height: 30.0)
|
|
||||||
let inset: CGFloat = 0.0
|
|
||||||
self.iconNode.frame = CGRect(origin: CGPoint(x: inset + 4.0, y: floor((constrainedSize.height - animationSize.height) / 2.0)), size: animationSize)
|
|
||||||
|
|
||||||
let size = CGSize(width: animationSize.width + inset * 2.0, height: constrainedSize.height)
|
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
|
||||||
self.buttonNode.frame = bounds
|
|
||||||
self.containerNode.frame = bounds
|
|
||||||
self.contextSourceNode.frame = bounds
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
@ -386,7 +76,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
private let peer: EnginePeer?
|
private let peer: EnginePeer?
|
||||||
private let chatLocation: ChatLocation?
|
private let chatLocation: ChatLocation?
|
||||||
|
|
||||||
private let titleView: MediaPickerSegmentedTitleView
|
private let titleView: MediaPickerTitleView
|
||||||
private let moreButtonNode: MediaPickerMoreButtonNode
|
private let moreButtonNode: MediaPickerMoreButtonNode
|
||||||
|
|
||||||
public weak var webSearchController: WebSearchController?
|
public weak var webSearchController: WebSearchController?
|
||||||
@ -939,6 +629,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if case let .noAccess(cameraAccess) = self.state {
|
if case let .noAccess(cameraAccess) = self.state {
|
||||||
|
var placeholderTransition = transition
|
||||||
let placeholderNode: MediaPickerPlaceholderNode
|
let placeholderNode: MediaPickerPlaceholderNode
|
||||||
if let current = self.placeholderNode {
|
if let current = self.placeholderNode {
|
||||||
placeholderNode = current
|
placeholderNode = current
|
||||||
@ -952,9 +643,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
self.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
|
self.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
|
||||||
self.placeholderNode = placeholderNode
|
self.placeholderNode = placeholderNode
|
||||||
|
|
||||||
|
if transition.isAnimated {
|
||||||
|
placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||||
}
|
}
|
||||||
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: transition)
|
placeholderTransition = .immediate
|
||||||
transition.updateFrame(node: placeholderNode, frame: bounds)
|
}
|
||||||
|
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
|
||||||
|
placeholderTransition.updateFrame(node: placeholderNode, frame: bounds)
|
||||||
} else if let placeholderNode = self.placeholderNode {
|
} else if let placeholderNode = self.placeholderNode {
|
||||||
self.placeholderNode = nil
|
self.placeholderNode = nil
|
||||||
placeholderNode.removeFromSupernode()
|
placeholderNode.removeFromSupernode()
|
||||||
@ -991,7 +687,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.chatLocation = chatLocation
|
self.chatLocation = chatLocation
|
||||||
|
|
||||||
self.titleView = MediaPickerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
|
self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
|
||||||
self.titleView.title = self.presentationData.strings.Attachment_Gallery
|
self.titleView.title = self.presentationData.strings.Attachment_Gallery
|
||||||
|
|
||||||
self.moreButtonNode = MediaPickerMoreButtonNode(theme: self.presentationData.theme)
|
self.moreButtonNode = MediaPickerMoreButtonNode(theme: self.presentationData.theme)
|
||||||
@ -1166,7 +862,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var mediaPickerContext: MediaPickerContext? {
|
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||||
if let interaction = self.controllerNode.interaction {
|
if let interaction = self.controllerNode.interaction {
|
||||||
return MediaPickerContext(interaction: interaction)
|
return MediaPickerContext(interaction: interaction)
|
||||||
} else {
|
} else {
|
||||||
@ -1175,10 +871,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MediaPickerContext: AttachmentMediaPickerContext {
|
final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||||
private weak var interaction: MediaPickerInteraction?
|
private weak var interaction: MediaPickerInteraction?
|
||||||
|
|
||||||
public var selectionCount: Signal<Int, NoError> {
|
var selectionCount: Signal<Int, NoError> {
|
||||||
return Signal { [weak self] subscriber in
|
return Signal { [weak self] subscriber in
|
||||||
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
|
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
|
||||||
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
|
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
|
||||||
@ -1189,7 +885,7 @@ public class MediaPickerContext: AttachmentMediaPickerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var caption: Signal<NSAttributedString?, NoError> {
|
var caption: Signal<NSAttributedString?, NoError> {
|
||||||
return Signal { [weak self] subscriber in
|
return Signal { [weak self] subscriber in
|
||||||
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
|
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
|
||||||
if let caption = caption as? NSAttributedString {
|
if let caption = caption as? NSAttributedString {
|
||||||
@ -1208,15 +904,15 @@ public class MediaPickerContext: AttachmentMediaPickerContext {
|
|||||||
self.interaction = interaction
|
self.interaction = interaction
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setCaption(_ caption: NSAttributedString) {
|
func setCaption(_ caption: NSAttributedString) {
|
||||||
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
|
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
|
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {
|
||||||
self.interaction?.sendSelected(nil, silently, nil, true)
|
self.interaction?.sendSelected(nil, silently, nil, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func schedule() {
|
func schedule() {
|
||||||
self.interaction?.schedule()
|
self.interaction?.schedule()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
94
submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift
Normal file
94
submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import SegmentedControlNode
|
||||||
|
|
||||||
|
final class MediaPickerTitleView: UIView {
|
||||||
|
private let titleNode: ImmediateTextNode
|
||||||
|
private let segmentedControlNode: SegmentedControlNode
|
||||||
|
|
||||||
|
public var theme: PresentationTheme {
|
||||||
|
didSet {
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||||
|
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var title: String = "" {
|
||||||
|
didSet {
|
||||||
|
if self.title != oldValue {
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: self.title, font: NavigationBar.titleFont, textColor: theme.rootController.navigationBar.primaryTextColor)
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var segmentsHidden = true {
|
||||||
|
didSet {
|
||||||
|
if self.segmentsHidden != oldValue {
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut)
|
||||||
|
transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
|
||||||
|
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
|
||||||
|
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var segments: [String] {
|
||||||
|
didSet {
|
||||||
|
if self.segments != oldValue {
|
||||||
|
self.segmentedControlNode.items = self.segments.map { SegmentedControlItem(title: $0) }
|
||||||
|
self.setNeedsLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var index: Int {
|
||||||
|
get {
|
||||||
|
return self.segmentedControlNode.selectedIndex
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self.segmentedControlNode.selectedIndex = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var indexUpdated: ((Int) -> Void)?
|
||||||
|
|
||||||
|
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
|
||||||
|
self.theme = theme
|
||||||
|
self.segments = segments
|
||||||
|
|
||||||
|
self.titleNode = ImmediateTextNode()
|
||||||
|
self.titleNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex)
|
||||||
|
self.segmentedControlNode.alpha = 0.0
|
||||||
|
self.segmentedControlNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
|
||||||
|
self?.indexUpdated?(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.segmentedControlNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
let size = self.bounds.size
|
||||||
|
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: 160.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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user